A lot of bug fixed

This commit is contained in:
stonegate 2020-04-01 00:36:20 +08:00
parent 62100085b0
commit a1d004aa43
31 changed files with 1787 additions and 1273 deletions

View File

@ -2,7 +2,7 @@
[![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/)
## About
<p align="center">
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png" art = "Logo"/>
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/ic_notification.png" art = "Logo"/>
</br>
<img src="https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-xhdpi/text.png" art = "Tsacdop"/>
</p>
@ -18,7 +18,7 @@ The podcasts search engine is powered by [ListenNotes](https://listennotes.com).
## License
Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.
Tsacdop is licensed under the [GPL V3.0](https://github.com/stonega/tsacdop/blob/master/LICENSE) license.
## Getting Started

View File

@ -0,0 +1,4 @@
eclipse.preferences.version=1
org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8
org.eclipse.jdt.core.compiler.compliance=1.8
org.eclipse.jdt.core.compiler.source=1.8

View File

@ -59,12 +59,10 @@ class PlayHistory {
}
}
class Playlist {
class Playlist extends ChangeNotifier {
String name;
DBHelper dbHelper = DBHelper();
// list of urls
//List<String> _urls;
//list of episodes
List<EpisodeBrief> _playlist;
//list of miediaitem
@ -73,14 +71,13 @@ class Playlist {
getPlaylist() async {
List<String> urls = await storage.getStringList();
print(urls);
if (urls.length == 0) {
_playlist = [];
} else {
_playlist = [];
await Future.forEach(urls, (url) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url);
if(episode != null) _playlist.add(episode);
if (episode != null) _playlist.add(episode);
});
}
print('Playlist: ' + _playlist.length.toString());
@ -89,7 +86,6 @@ class Playlist {
savePlaylist() async {
List<String> urls = [];
urls.addAll(_playlist.map((e) => e.enclosureUrl));
print(urls);
await storage.saveStringList(urls);
}
@ -103,10 +99,12 @@ class Playlist {
await savePlaylist();
}
delFromPlaylist(EpisodeBrief episodeBrief) async {
Future<int> delFromPlaylist(EpisodeBrief episodeBrief) async {
int index = _playlist.indexOf(episodeBrief);
_playlist
.removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl);
await savePlaylist();
return index;
}
}
@ -115,6 +113,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
KeyValueStorage storage = KeyValueStorage('audioposition');
EpisodeBrief _episode;
Playlist _queue = Playlist();
bool _queueUpdate = false;
BasicPlaybackState _audioState = BasicPlaybackState.none;
bool _playerRunning = false;
bool _noSlide = true;
@ -127,7 +126,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool _stopOnComplete = false;
Timer _stopTimer;
int _timeLeft = 0;
//Show stopwatch after user setting timer.
bool _showStopWatch = false;
double _switchValue = 0;
bool _autoPlay = true;
@ -143,6 +141,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
bool get playerRunning => _playerRunning;
int get lastPositin => _lastPostion;
Playlist get queue => _queue;
bool get queueUpdate => _queueUpdate;
EpisodeBrief get episode => _episode;
bool get stopOnComplete => _stopOnComplete;
bool get showStopWatch => _showStopWatch;
@ -155,7 +154,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners();
}
set autoPlaySwitch(bool boo) {
set autoPlaySwitch(bool boo) {
_autoPlay = boo;
notifyListeners();
}
@ -163,38 +162,50 @@ class AudioPlayerNotifier extends ChangeNotifier {
@override
void addListener(VoidCallback listener) async {
super.addListener(listener);
_queueUpdate = false;
await AudioService.connect();
if(await AudioService.running){
AudioService.stop();
bool running = await AudioService.running;
if (running) {
await AudioService.pause();
}
}
loadPlaylist() async {
await _queue.getPlaylist();
_lastPostion = await storage.getInt();
if (_lastPostion > 0 && _queue.playlist.length > 0) {
final EpisodeBrief episode = _queue.playlist.first;
final int duration = episode.enclosureLength * 60;
final double seekValue = duration != 0 ? _lastPostion / duration : 1;
final PlayHistory history = PlayHistory(
episode.title, episode.enclosureUrl, _lastPostion / 1000, seekValue);
await dbHelper.saveHistory(history);
}
}
episodeLoad(EpisodeBrief episode) async {
final EpisodeBrief episodeNew =
await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
if (_playerRunning) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition / 1000, seekSliderValue);
await dbHelper.saveHistory(history);
AudioService.addQueueItemAt(episode.toMediaItem(), 0);
AudioService.addQueueItemAt(episodeNew.toMediaItem(), 0);
_queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode);
_queue.playlist.insert(0, episodeNew);
notifyListeners();
await _queue.savePlaylist();
} else {
await _queue.getPlaylist();
_queue.playlist
.removeWhere((item) => item.enclosureUrl == episode.enclosureUrl);
_queue.playlist.insert(0, episode);
_queue.playlist.insert(0, episodeNew);
_queue.savePlaylist();
_backgroundAudioDuration = 0;
_backgroundAudioPosition = 0;
_seekSliderValue = 0;
_episode = episode;
_episode = episodeNew;
_playerRunning = true;
notifyListeners();
await _queue.savePlaylist();
@ -207,13 +218,13 @@ class AudioPlayerNotifier extends ChangeNotifier {
await AudioService.connect();
}
await AudioService.start(
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Tsacdop',
notificationColor: 0xFF2196f3,
androidNotificationIcon: 'drawable/ic_notification',
enableQueue: true,
androidStopOnRemoveTask: true,
);
backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint,
androidNotificationChannelName: 'Tsacdop',
notificationColor: 0xFF4d91be,
androidNotificationIcon: 'drawable/ic_notification',
enableQueue: true,
androidStopOnRemoveTask: true,
androidStopForegroundOnPause: true);
_playerRunning = true;
if (_autoPlay) {
await Future.forEach(_queue.playlist, (episode) async {
@ -242,45 +253,58 @@ class AudioPlayerNotifier extends ChangeNotifier {
print(_episode.title);
_queue.delFromPlaylist(_episode);
}
if (_audioState == BasicPlaybackState.paused ||
_audioState == BasicPlaybackState.skippingToNext &&
_episode != null) {
if (_audioState == BasicPlaybackState.skippingToNext &&
_episode != null &&
_backgroundAudioPosition > 0) {
PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl,
backgroundAudioPosition / 1000, seekSliderValue);
_backgroundAudioPosition / 1000, _seekSliderValue);
await dbHelper.saveHistory(history);
}
if (_audioState == BasicPlaybackState.stopped) {
_playerRunning = false;
if (_audioState == BasicPlaybackState.stopped) _playerRunning = false;
if (_audioState == BasicPlaybackState.error) {
_remoteErrorMessage = 'Network Error';
}
if (_audioState != BasicPlaybackState.error &&
_audioState != BasicPlaybackState.paused) {
_remoteErrorMessage = null;
}
_currentPosition = event?.currentPosition ?? 0;
notifyListeners();
});
Timer.periodic(Duration(milliseconds: 500), (timer) {
if (_noSlide) {
_audioState == BasicPlaybackState.playing
? (_backgroundAudioPosition < _backgroundAudioDuration - 500)
? _backgroundAudioPosition = _currentPosition +
DateTime.now().difference(_current).inMilliseconds
: _backgroundAudioPosition = _backgroundAudioDuration
: _backgroundAudioPosition = _currentPosition;
if (_audioState == BasicPlaybackState.playing) {
if (_backgroundAudioPosition < _backgroundAudioDuration - 500)
_backgroundAudioPosition = _currentPosition +
DateTime.now().difference(_current).inMilliseconds;
else
_backgroundAudioPosition = _backgroundAudioDuration;
} else
_backgroundAudioPosition = _currentPosition;
if (_backgroundAudioDuration != null &&
_backgroundAudioDuration != 0 &&
_backgroundAudioPosition != null) {
_seekSliderValue =
_backgroundAudioPosition / _backgroundAudioDuration ?? 0;
}
} else
_seekSliderValue = 0;
if (_backgroundAudioPosition > 0) {
_lastPostion = _backgroundAudioPosition;
storage.saveInt(_lastPostion);
}
if ((_queue.playlist.length == 1 || !_autoPlay) &&
_seekSliderValue == 1 &&
_seekSliderValue > 0.9 &&
_episode != null) {
_queue.delFromPlaylist(_episode);
_lastPostion = 0;
storage.saveInt(_lastPostion);
PlayHistory history = PlayHistory(
final PlayHistory history = PlayHistory(
_episode.title,
_episode.enclosureUrl,
backgroundAudioPosition / 1000,
@ -325,6 +349,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
print('add to playlist when not rnnning');
await _queue.addToPlayListAt(episode, index);
_queueUpdate = !_queueUpdate;
notifyListeners();
}
@ -332,17 +357,20 @@ class AudioPlayerNotifier extends ChangeNotifier {
int index = _queue.playlist
.indexWhere((item) => item.enclosureUrl == episode.enclosureUrl);
if (index > 0) {
EpisodeBrief episodeNew =
await dbHelper.getRssItemWithUrl(episode.enclosureUrl);
await delFromPlaylist(episode);
await addToPlaylistAt(episode, index);
await addToPlaylistAt(episodeNew, index);
}
}
delFromPlaylist(EpisodeBrief episode) async {
Future<int> delFromPlaylist(EpisodeBrief episode) async {
if (_playerRunning) {
await AudioService.removeQueueItem(episode.toMediaItem());
}
await _queue.delFromPlaylist(episode);
int index = await _queue.delFromPlaylist(episode);
notifyListeners();
return index;
}
moveToTop(EpisodeBrief episode) async {
@ -354,6 +382,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
_lastPostion = 0;
storage.saveInt(_lastPostion);
}
notifyListeners();
}
pauseAduio() async {
@ -361,22 +390,32 @@ class AudioPlayerNotifier extends ChangeNotifier {
}
resumeAudio() async {
AudioService.play();
if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none) AudioService.play();
}
forwardAudio(int s) {
int pos = _backgroundAudioPosition + s * 1000;
AudioService.seekTo(pos);
}
seekTo(int position) async{
if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none)
await AudioService.seekTo(position);
}
sliderSeek(double val) async {
print(val.toString());
_noSlide = false;
_seekSliderValue = val;
notifyListeners();
_currentPosition = (val * _backgroundAudioDuration).toInt();
await AudioService.seekTo(_currentPosition);
_noSlide = true;
if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none) {
_noSlide = false;
_seekSliderValue = val;
notifyListeners();
_currentPosition = (val * _backgroundAudioDuration).toInt();
await AudioService.seekTo(_currentPosition);
_noSlide = true;
}
}
//Set sleep time
@ -459,6 +498,10 @@ class AudioPlayerTask extends BackgroundAudioTask {
_handlePlaybackCompleted();
});
var eventSubscription = _audioPlayer.playbackEventStream.listen((event) {
print('buffer position' + event.bufferedPosition.toString());
if (event.playbackError != null) {
_setState(state: BasicPlaybackState.error);
}
BasicPlaybackState state;
if (event.buffering) {
state = BasicPlaybackState.buffering;
@ -514,7 +557,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
_skipState = BasicPlaybackState.skippingToNext;
await _audioPlayer.setUrl(mediaItem.id);
print(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture ?? 0;
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
_skipState = null;
@ -590,7 +633,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
AudioServiceBackground.setQueue(_queue);
AudioServiceBackground.setMediaItem(mediaItem);
await _audioPlayer.setUrl(mediaItem.id);
Duration duration = await _audioPlayer.durationFuture;
Duration duration = await _audioPlayer.durationFuture ?? Duration.zero;
AudioServiceBackground.setMediaItem(
mediaItem.copyWith(duration: duration.inMilliseconds));
onPlay();

View File

@ -0,0 +1,191 @@
import 'dart:io';
import 'dart:isolate';
import 'dart:ui';
import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'episodebrief.dart';
class EpisodeTask {
final String taskId;
int progress;
DownloadTaskStatus status;
final EpisodeBrief episode;
EpisodeTask(
this.episode,
this.taskId, {
this.progress = 0,
this.status = DownloadTaskStatus.undefined,
});
}
void downloadCallback(String id, DownloadTaskStatus status, int progress) {
print('Homepage callback task in $id status ($status) $progress');
final SendPort send =
IsolateNameServer.lookupPortByName('downloader_send_port');
send.send([id, status, progress]);
}
class DownloadState extends ChangeNotifier {
DBHelper dbHelper = DBHelper();
List<EpisodeTask> _episodeTasks = [];
List<EpisodeTask> get episodeTasks => _episodeTasks;
@override
void addListener(VoidCallback listener) async {
_loadTasks();
_bindBackgroundIsolate();
FlutterDownloader.registerCallback(downloadCallback);
super.addListener(listener);
}
_loadTasks() async {
_episodeTasks = [];
DBHelper dbHelper = DBHelper();
var tasks = await FlutterDownloader.loadTasks();
if (tasks.length != 0)
await Future.forEach(tasks, (DownloadTask task) async {
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.url);
_episodeTasks.add(EpisodeTask(episode, task.taskId,
progress: task.progress, status: task.status));
});
print(_episodeTasks.length);
notifyListeners();
}
void _bindBackgroundIsolate() {
ReceivePort _port = ReceivePort();
bool isSuccess = IsolateNameServer.registerPortWithName(
_port.sendPort, 'downloader_send_port');
if (!isSuccess) {
_unbindBackgroundIsolate();
_bindBackgroundIsolate();
return;
}
_port.listen((dynamic data) {
String id = data[0];
DownloadTaskStatus status = data[1];
int progress = data[2];
_episodeTasks.forEach((episodeTask) {
if (episodeTask.taskId == id) {
episodeTask.status = status;
episodeTask.progress = progress;
if (status == DownloadTaskStatus.complete) {
_saveMediaId(episodeTask).then((value) {
notifyListeners();
});
} else
notifyListeners();
}
});
});
}
Future _saveMediaId(EpisodeTask episodeTask) async {
episodeTask.status = DownloadTaskStatus.complete;
final completeTask = await FlutterDownloader.loadTasksWithRawQuery(
query:
"SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'");
String filePath = 'file://' +
path.join(completeTask.first.savedDir, completeTask.first.filename);
print(filePath);
dbHelper.saveMediaId(
episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId);
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port');
}
EpisodeTask episodeToTask(EpisodeBrief episode) {
return _episodeTasks
.firstWhere((task) => task.episode.enclosureUrl == episode.enclosureUrl,
orElse: () {
return EpisodeTask(
episode,
'',
);
});
}
@override
void dispose() {
_unbindBackgroundIsolate();
super.dispose();
}
Future startTask(EpisodeBrief episode) async {
final dir = await getExternalStorageDirectory();
String localPath = path.join(dir.path, episode.feedTitle);
final saveDir = Directory(localPath);
bool hasExisted = await saveDir.exists();
if (!hasExisted) {
saveDir.create();
}
String taskId = await FlutterDownloader.enqueue(
url: episode.enclosureUrl,
savedDir: localPath,
showNotification: true,
openFileFromNotification: false,
);
_episodeTasks.add(EpisodeTask(episode, taskId));
notifyListeners();
}
Future pauseTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
await FlutterDownloader.pause(taskId: task.taskId);
}
Future resumeTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
String newTaskId = await FlutterDownloader.resume(taskId: task.taskId);
int index = _episodeTasks.indexOf(task);
_removeTask(episode);
FlutterDownloader.remove(taskId: task.taskId);
var dbHelper = DBHelper();
_episodeTasks.insert(index, EpisodeTask(episode, newTaskId));
await dbHelper.saveDownloaded(newTaskId, episode.enclosureUrl);
}
Future retryTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
String newTaskId = await FlutterDownloader.retry(taskId: task.taskId);
await FlutterDownloader.remove(taskId: task.taskId);
int index = _episodeTasks.indexOf(task);
_removeTask(episode);
var dbHelper = DBHelper();
_episodeTasks.insert(index, EpisodeTask(episode, newTaskId));
await dbHelper.saveDownloaded(newTaskId, episode.enclosureUrl);
}
Future removeTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
await FlutterDownloader.remove(
taskId: task.taskId, shouldDeleteContent: false);
}
Future delTask(EpisodeBrief episode) async {
EpisodeTask task = episodeToTask(episode);
await FlutterDownloader.remove(
taskId: task.taskId, shouldDeleteContent: true);
await dbHelper.delDownloaded(episode.enclosureUrl);
_episodeTasks.forEach((episodeTask) {
if (episodeTask.taskId == task.taskId)
episodeTask.status = DownloadTaskStatus.undefined;
notifyListeners();
});
_removeTask(episode);
}
_removeTask(EpisodeBrief episode) {
_episodeTasks.removeWhere(
(element) => element.episode.enclosureUrl == episode.enclosureUrl);
}
}

View File

@ -1,18 +0,0 @@
import 'package:flutter/foundation.dart';
enum DownloadState { stop, load, donwload, complete, error }
class EpisodeDownload with ChangeNotifier {
String _title;
String get title => _title;
set title(String t) {
_title = t;
notifyListeners();
}
DownloadState _downloadState = DownloadState.stop;
DownloadState get downloadState => _downloadState;
set downloadState(DownloadState state){
_downloadState = state;
notifyListeners();
}
}

View File

@ -15,6 +15,7 @@ class EpisodeBrief {
final int explicit;
final String imagePath;
final String mediaId;
final int isNew;
EpisodeBrief(
this.title,
this.enclosureUrl,
@ -27,7 +28,8 @@ class EpisodeBrief {
this.duration,
this.explicit,
this.imagePath,
this.mediaId);
this.mediaId,
this.isNew);
String dateToString() {
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true);

View File

@ -21,7 +21,6 @@ class GroupEntity {
static GroupEntity fromJson(Map<String, Object> json) {
List<String> list = List.from(json['podcastList']);
print(json['[podcastList']);
return GroupEntity(json['name'] as String, json['id'] as String,
json['color'] as String, list);
}
@ -58,6 +57,7 @@ class PodcastGroup {
List<PodcastLocal> _orderedPodcasts;
List<PodcastLocal> get ordereddPodcasts => _orderedPodcasts;
List<PodcastLocal> get podcasts => _podcasts;
set setOrderedPodcasts(List<PodcastLocal> list) {
_orderedPodcasts = list;
}
@ -88,18 +88,29 @@ class GroupList extends ChangeNotifier {
bool _isLoading = false;
bool get isLoading => _isLoading;
List<String> _orderChanged = [];
List<String> get orderChanged => _orderChanged;
void addToOrderChanged(String name) {
_orderChanged.add(name);
List<PodcastGroup> _orderChanged = [];
List<PodcastGroup> get orderChanged => _orderChanged;
void addToOrderChanged(PodcastGroup group) {
_orderChanged.add(group);
notifyListeners();
}
void drlFromOrderChanged(String name) {
_orderChanged.remove(name);
_orderChanged.removeWhere((group) => group.name == name);
notifyListeners();
}
clearOrderChanged() async {
if (_orderChanged.length > 0) {
await Future.forEach(_orderChanged, (PodcastGroup group) async {
await group.getPodcasts();
});
_orderChanged.clear();
// notifyListeners();
}
}
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
@ -162,6 +173,18 @@ class GroupList extends ChangeNotifier {
notifyListeners();
}
Future updatePodcast(PodcastLocal podcastLocal) async {
List<int> counts = await dbHelper.getPodcastCounts(podcastLocal.id);
_groups.forEach((group) {
if (group.podcastList.contains(podcastLocal.id)) {
group.podcasts.firstWhere((podcast) => podcast.id == podcastLocal.id)
..upateCount = counts[0]
..episodeCount = counts[1];
notifyListeners();
}
});
}
List<PodcastGroup> getPodcastGroup(String id) {
List<PodcastGroup> result = [];
_groups.forEach((group) {

View File

@ -11,8 +11,8 @@ class PodcastLocal {
final String link;
final String description;
final int upateCount;
final int episodeCount;
int upateCount;
int episodeCount;
PodcastLocal(
this.title,
this.imageUrl,

View File

@ -10,24 +10,23 @@ void callbackDispatcher() {
Workmanager.executeTask((task, inputData) async {
var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
int i = 0;
await Future.forEach(podcastList, (podcastLocal) async {
i += await dbHelper.updatePodcastRss(podcastLocal);
await dbHelper.updatePodcastRss(podcastLocal);
print('Refresh ' + podcastLocal.title);
});
KeyValueStorage refreshstorage = KeyValueStorage('refreshdate');
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
await refreshcountstorage.saveInt(i);
return Future.value(true);
});
}
class SettingState extends ChangeNotifier {
KeyValueStorage themestorage = KeyValueStorage('themes');
KeyValueStorage accentstorage = KeyValueStorage('accents');
KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate');
KeyValueStorage intervalstorage = KeyValueStorage('updateInterval');
KeyValueStorage themeStorage = KeyValueStorage('themes');
KeyValueStorage accentStorage = KeyValueStorage('accents');
KeyValueStorage autoupdateStorage = KeyValueStorage('autoupdate');
KeyValueStorage intervalStorage = KeyValueStorage('updateInterval');
KeyValueStorage downloadUsingDataStorage =
KeyValueStorage('downloadUsingData');
Future initData() async {
await _getTheme();
@ -49,7 +48,7 @@ class SettingState extends ChangeNotifier {
_saveUpdateInterval();
Workmanager.initialize(
callbackDispatcher,
isInDebugMode: true,
isInDebugMode: false,
);
Workmanager.registerPeriodicTask("1", "update_podcasts",
frequency: Duration(hours: hour),
@ -60,8 +59,8 @@ class SettingState extends ChangeNotifier {
print('work manager init done + ');
}
void cancelWork() {
Workmanager.cancelAll();
Future cancelWork() async{
await Workmanager.cancelAll();
print('work job cancelled');
}
@ -86,58 +85,74 @@ class SettingState extends ChangeNotifier {
notifyListeners();
}
bool _downloadUsingData;
bool get downloadUsingData => _downloadUsingData;
set downloadUsingData(bool boo) {
_downloadUsingData = boo;
_saveDownloadUsingData();
notifyListeners();
}
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
_getTheme();
_getAccentSetColor();
_getAutoUpdate();
_getDownloadUsingData();
_getUpdateInterval().then((value) {
if (_initUpdateTag == 0) setWorkManager(24);
});
}
Future _getTheme() async {
int mode = await themestorage.getInt();
int mode = await themeStorage.getInt();
_theme = ThemeMode.values[mode];
}
Future _saveTheme() async {
await themestorage.saveInt(_theme.index);
await themeStorage.saveInt(_theme.index);
}
Future _getAccentSetColor() async {
String colorString = await accentstorage.getString();
print(colorString);
String colorString = await accentStorage.getString();
if (colorString.isNotEmpty) {
int color = int.parse('FF' + colorString.toUpperCase(), radix: 16);
_accentSetColor = Color(color).withOpacity(1.0);
print(_accentSetColor.toString());
} else {
_accentSetColor = Colors.blue[400];
}
}
Future _saveAccentSetColor() async {
await accentstorage
await accentStorage
.saveString(_accentSetColor.toString().substring(10, 16));
}
Future _getAutoUpdate() async {
int i = await autoupdatestorage.getInt();
int i = await autoupdateStorage.getInt();
_autoUpdate = i == 0 ? true : false;
}
Future _saveAutoUpdate() async {
await autoupdatestorage.saveInt(_autoUpdate ? 0 : 1);
await autoupdateStorage.saveInt(_autoUpdate ? 0 : 1);
}
Future _getUpdateInterval() async {
_initUpdateTag = await intervalstorage.getInt();
_updateInterval = _initUpdateTag;
_initUpdateTag = await intervalStorage.getInt();
_updateInterval = _initUpdateTag;
}
Future _saveUpdateInterval() async {
await intervalstorage.saveInt(_updateInterval);
await intervalStorage.saveInt(_updateInterval);
}
Future _getDownloadUsingData() async {
int i = await downloadUsingDataStorage.getInt();
_downloadUsingData = i == 0 ? true : false;
}
Future _saveDownloadUsingData() async {
await downloadUsingDataStorage.saveInt(_downloadUsingData ? 0 : 1);
}
}

View File

@ -5,6 +5,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_html/flutter_html.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/home/audioplayer.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart';
@ -382,7 +383,7 @@ class _MenuBarState extends State<MenuBar> {
() => setUnliked(widget.episodeItem.enclosureUrl)),
],
),
DownloadButton(episodeBrief: widget.episodeItem),
DownloadButton(episode: widget.episodeItem),
Selector<AudioPlayerNotifier, List<String>>(
selector: (_, audio) =>
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),

View File

@ -1,193 +1,95 @@
import 'dart:isolate';
import 'dart:ui';
import 'dart:io';
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path;
import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:connectivity/connectivity.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/class/settingstate.dart';
class DownloadButton extends StatefulWidget {
final EpisodeBrief episodeBrief;
DownloadButton({this.episodeBrief, Key key}) : super(key: key);
final EpisodeBrief episode;
DownloadButton({this.episode, Key key}) : super(key: key);
@override
_DownloadButtonState createState() => _DownloadButtonState();
}
class _DownloadButtonState extends State<DownloadButton> {
_TaskInfo _task;
bool _isLoading;
bool _permissionReady;
String _localPath;
ReceivePort _port = ReceivePort();
Future<String> _getPath() async {
final dir = await getExternalStorageDirectory();
return dir.path;
}
bool _usingData;
StreamSubscription _connectivity;
EpisodeTask _task;
@override
void initState() {
super.initState();
_bindBackgroundIsolate();
FlutterDownloader.registerCallback(downloadCallback);
_isLoading = true;
_permissionReady = false;
_prepare();
_connectivity = Connectivity().onConnectivityChanged.listen((result) {
_usingData = result == ConnectivityResult.mobile;
});
}
@override
void dispose() {
_unbindBackgroundIsolate();
_connectivity.cancel();
super.dispose();
}
void _bindBackgroundIsolate() {
bool isSuccess = IsolateNameServer.registerPortWithName(
_port.sendPort, 'downloader_send_port');
if (!isSuccess) {
_unbindBackgroundIsolate();
_bindBackgroundIsolate();
return;
}
_port.listen((dynamic data) {
print('UI isolate callback: $data');
String id = data[0];
DownloadTaskStatus status = data[1];
int progress = data[2];
if (_task.taskId == id) {
print(_task.progress);
setState(() {
_task.status = status;
_task.progress = progress;
});
}
});
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port');
}
static void downloadCallback(
String id, DownloadTaskStatus status, int progress) {
print('Background callback task in $id status ($status) $progress');
final SendPort send =
IsolateNameServer.lookupPortByName('downloader_send_port');
send.send([id, status, progress]);
}
void _requestDownload(_TaskInfo task) async {
void _requestDownload(EpisodeBrief episode, bool downloadUsingData) async {
_permissionReady = await _checkPermmison();
if (_permissionReady)
task.taskId = await FlutterDownloader.enqueue(
url: task.link,
savedDir: _localPath,
showNotification: true,
openFileFromNotification: false,
);
var dbHelper = DBHelper();
await dbHelper.saveDownloaded(task.link, task.taskId);
Fluttertoast.showToast(
msg: 'Downloading',
gravity: ToastGravity.BOTTOM,
);
bool _dataConfirm = true;
if (_permissionReady) {
if (downloadUsingData && _usingData) {
_dataConfirm = await _useDataConfirem();
}
if (_dataConfirm) {
Provider.of<DownloadState>(context, listen: false).startTask(episode);
Fluttertoast.showToast(
msg: 'Downloading',
gravity: ToastGravity.BOTTOM,
);
}
}
}
void _deleteDownload(_TaskInfo task) async {
await FlutterDownloader.remove(
taskId: task.taskId, shouldDeleteContent: true);
var dbHelper = DBHelper();
await dbHelper.delDownloaded(task.link);
await _prepare();
setState(() {});
void _deleteDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).delTask(episode);
Fluttertoast.showToast(
msg: 'Download removed',
gravity: ToastGravity.BOTTOM,
);
}
void _pauseDownload(_TaskInfo task) async {
await FlutterDownloader.pause(taskId: task.taskId);
void _pauseDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).pauseTask(episode);
Fluttertoast.showToast(
msg: 'Download paused',
gravity: ToastGravity.BOTTOM,
);
}
void _resumeDownload(_TaskInfo task) async {
String newTaskId = await FlutterDownloader.resume(taskId: task.taskId);
task.taskId = newTaskId;
var dbHelper = DBHelper();
await dbHelper.saveDownloaded(task.taskId, task.link);
void _resumeDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).resumeTask(episode);
Fluttertoast.showToast(
msg: 'Download resumed',
gravity: ToastGravity.BOTTOM,
);
}
void _retryDownload(_TaskInfo task) async {
String newTaskId = await FlutterDownloader.retry(taskId: task.taskId);
task.taskId = newTaskId;
var dbHelper = DBHelper();
await dbHelper.saveDownloaded(task.taskId, task.link);
void _retryDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).retryTask(episode);
Fluttertoast.showToast(
msg: 'Download again',
gravity: ToastGravity.BOTTOM,
);
}
Future<EpisodeBrief> _saveMediaId(_TaskInfo task) async {
final completeTask = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE url = '${task.link}'");
String filePath = 'file://' +
path.join(completeTask.first.savedDir, completeTask.first.filename);
var dbHelper = DBHelper();
await dbHelper.saveMediaId(task.link, filePath);
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.link);
return episode;
}
Future<Null> _prepare() async {
final tasks = await FlutterDownloader.loadTasks();
_task = _TaskInfo(
name: widget.episodeBrief.title,
link: widget.episodeBrief.enclosureUrl,
);
tasks?.forEach((task) {
if (_task.link == task.url) {
_task.taskId = task.taskId;
_task.status = task.status;
_task.progress = task.progress;
}
});
_localPath = path.join((await _getPath()), widget.episodeBrief.feedTitle);
print(_localPath);
final saveDir = Directory(_localPath);
bool hasExisted = await saveDir.exists();
if (!hasExisted) {
saveDir.create();
}
setState(() {
_isLoading = false;
});
}
Future<bool> _checkPermmison() async {
PermissionStatus permission = await PermissionHandler()
.checkPermissionStatus(PermissionGroup.storage);
@ -205,6 +107,60 @@ class _DownloadButtonState extends State<DownloadButton> {
}
}
Future<bool> _useDataConfirem() async {
bool ifUseData = false;
await showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration: const Duration(milliseconds: 200),
pageBuilder: (BuildContext context, Animation animaiton,
Animation secondaryAnimation) =>
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
systemNavigationBarColor:
Theme.of(context).brightness == Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
),
child: AlertDialog(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10.0))),
titlePadding:
EdgeInsets.only(top: 20, left: 20, right: 100, bottom: 20),
title: Text('Cellular data warn'),
content:
Text('Are you sure you want to use cellular data to download?'),
actions: <Widget>[
FlatButton(
onPressed: () {
Navigator.of(context).pop();
},
child: Text(
'CANCEL',
style: TextStyle(color: Colors.grey[600]),
),
),
FlatButton(
onPressed: () {
ifUseData = true;
Navigator.of(context).pop();
},
child: Text(
'CONFIRM',
style: TextStyle(color: Colors.red),
),
)
],
),
),
);
return ifUseData;
}
Widget _buttonOnMenu(Widget widget, Function() onTap) => Material(
color: Colors.transparent,
child: InkWell(
@ -218,112 +174,111 @@ class _DownloadButtonState extends State<DownloadButton> {
@override
Widget build(BuildContext context) {
return _isLoading
? Center()
: Row(
children: <Widget>[
_downloadButton(_task),
AnimatedContainer(
duration: Duration(seconds: 1),
decoration: BoxDecoration(
color: Theme.of(context).accentColor,
borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 20.0,
width:
(_task.status == DownloadTaskStatus.running) ? 50.0 : 0,
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text('${_task.progress}%',
style: TextStyle(color: Colors.white)),
)),
],
);
return Consumer<DownloadState>(builder: (_, downloader, __) {
EpisodeTask _task = Provider.of<DownloadState>(context, listen: false)
.episodeToTask(widget.episode);
return Row(
children: <Widget>[
_downloadButton(_task, context),
AnimatedContainer(
duration: Duration(seconds: 1),
decoration: BoxDecoration(
color: Theme.of(context).accentColor,
borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 20.0,
width: (_task.status == DownloadTaskStatus.running) ? 50.0 : 0,
alignment: Alignment.center,
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Text('${_task.progress}%',
style: TextStyle(color: Colors.white)),
)),
],
);
});
}
Widget _downloadButton(_TaskInfo task) {
if (task.status == DownloadTaskStatus.undefined) {
return _buttonOnMenu(
Icon(
Icons.arrow_downward,
color: Colors.grey[700],
),
() => _requestDownload(task));
} else if (task.status == DownloadTaskStatus.running) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
if (task.progress > 0) _pauseDownload(task);
},
child: Container(
height: 50.0,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 18.0),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).accentColor),
value: task.progress / 100,
Widget _downloadButton(EpisodeTask task, BuildContext context) {
switch (task.status.value) {
case 0:
return Selector<SettingState, bool>(
selector: (_, settings) => settings.downloadUsingData,
builder: (_, data, __) => _buttonOnMenu(
Icon(
Icons.arrow_downward,
color: Colors.grey[700],
),
() => _requestDownload(task.episode, data)),
);
break;
case 2:
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
if (task.progress > 0) _pauseDownload(task.episode);
},
child: Container(
height: 50.0,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 18.0),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(
Theme.of(context).accentColor),
value: task.progress / 100,
),
),
),
),
),
);
} else if (task.status == DownloadTaskStatus.paused) {
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_resumeDownload(task);
},
child: Container(
height: 50.0,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 18),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
value: task.progress / 100,
);
break;
case 6:
return Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_resumeDownload(task.episode);
},
child: Container(
height: 50.0,
alignment: Alignment.center,
padding: EdgeInsets.symmetric(horizontal: 18),
child: SizedBox(
height: 18,
width: 18,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[500],
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
value: task.progress / 100,
),
),
),
),
),
);
} else if (task.status == DownloadTaskStatus.complete) {
if(!widget.episodeBrief.mediaId.contains('file://'))
{_saveMediaId(task).then((episode) =>
Provider.of<AudioPlayerNotifier>(context, listen: false)
.updateMediaItem(episode));}
return _buttonOnMenu(
Icon(
Icons.done_all,
color: Theme.of(context).accentColor,
),
() => _deleteDownload(task));
} else if (task.status == DownloadTaskStatus.failed) {
return _buttonOnMenu(
Icon(Icons.refresh, color: Colors.red), () => _retryDownload(task));
);
break;
case 3:
Provider.of<AudioPlayerNotifier>(context, listen: false)
.updateMediaItem(task.episode);
return _buttonOnMenu(
Icon(
Icons.done_all,
color: Theme.of(context).accentColor,
), () {
_deleteDownload(task.episode);
});
break;
case 4:
return _buttonOnMenu(Icon(Icons.refresh, color: Colors.red),
() => _retryDownload(task.episode));
break;
default:
return Center();
}
return Center();
}
}
class _TaskInfo {
final String name;
final String link;
String taskId;
int progress = 0;
DownloadTaskStatus status = DownloadTaskStatus.undefined;
_TaskInfo({this.name, this.link});
}

View File

@ -80,7 +80,7 @@ class AboutApp extends StatelessWidget {
padding: EdgeInsets.symmetric(horizontal: 50),
height: 50,
child: Text(
'Tsacdop is a podcasts client developed in flutter, a simple, beautiful, and easy-use application.',
'Tsacdop is a podcast player developed in flutter, a simple, beautiful, and easy-use application.',
textAlign: TextAlign.center,
),
),
@ -110,12 +110,12 @@ class AboutApp extends StatelessWidget {
_listItem(context, 'GitHub', LineIcons.github,
'https://github.com/stonaga/'),
_listItem(context, 'Twitter', LineIcons.twitter,
'https://twitter.com'),
'https://twitter.com/shimenmen'),
_listItem(
context,
'Stone Gate',
LineIcons.hat_cowboy_solid,
'mailto:<xijieyin@gmail.com>?subject=Tsacdop Feedback'),
LineIcons.medium,
'https://medium.com/@stonegate'),
],
),
),

View File

@ -91,7 +91,6 @@ class _MyHomePageState extends State<MyHomePage> {
class _MyHomePageDelegate extends SearchDelegate<int> {
static Future<List> getList(String searchText) async {
String apiKey = environment['apiKey'];
print(apiKey);
String url =
"https://listennotes.p.mashape.com/api/v1/search?only_in=title%2Cdescription&q=" +
searchText +
@ -312,7 +311,7 @@ class _SearchResultState extends State<SearchResult> {
importOmpl.importState = ImportState.parse;
await dbHelper.savePodcastRss(_p, _uuid);
groupList.updatePodcast(podcastLocal);
importOmpl.importState = ImportState.complete;
} else {
importOmpl.importState = ImportState.error;

View File

@ -173,7 +173,7 @@ class _PopupMenuState extends State<PopupMenu> {
importOmpl.importState = ImportState.parse;
await dbHelper.savePodcastRss(_p, _uuid);
groupList.updatePodcast(podcastLocal);
importOmpl.importState = ImportState.complete;
} else {
importOmpl.importState = ImportState.error;

View File

@ -5,8 +5,11 @@ import 'dart:math' as math;
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:marquee/marquee.dart';
import 'package:tsacdop/home/playlist.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tuple/tuple.dart';
import 'package:audio_service/audio_service.dart';
import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/class/audiostate.dart';
@ -15,6 +18,7 @@ import 'package:tsacdop/home/audiopanel.dart';
import 'package:tsacdop/util/pageroute.dart';
import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/day_night_switch.dart';
import 'package:tsacdop/util/context_extension.dart';
class MyRectangularTrackShape extends RectangularSliderTrackShape {
Rect getPreferredRect({
@ -143,18 +147,13 @@ class _PlayerWidgetState extends State<PlayerWidget> {
List minsToSelect = [1, 5, 10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99];
int _minSelected;
final GlobalKey<AnimatedListState> _miniPlaylistKey = GlobalKey();
@override
void initState() {
super.initState();
_minSelected = 30;
}
@override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
}
Widget _sleppMode(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Selector<AudioPlayerNotifier, Tuple3<bool, int, double>>(
@ -571,9 +570,14 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Container(
padding: EdgeInsets.symmetric(horizontal: 5.0),
width: 200,
child: Text(data.item1.feedTitle, maxLines: 1, overflow: TextOverflow.fade,),
child: Text(
data.item1.feedTitle,
maxLines: 1,
overflow: TextOverflow.fade,
),
),
Spacer(),
LastPosition(),
IconButton(
onPressed: () => Navigator.push(
context,
@ -596,138 +600,252 @@ class _PlayerWidgetState extends State<PlayerWidget> {
Widget _playlist(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Container(
alignment: Alignment.bottomLeft,
child: AnimatedContainer(
duration: Duration(milliseconds: 400),
height: 300,
width: MediaQuery.of(context).size.width,
alignment: Alignment.center,
// margin: EdgeInsets.all(20),
//padding: EdgeInsets.only(bottom: 10.0),
decoration: BoxDecoration(
// borderRadius: BorderRadius.all(Radius.circular(10.0)),
color: Theme.of(context).primaryColor,
),
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist,
builder: (_, playlist, __) {
return ListView.builder(
shrinkWrap: true,
scrollDirection: Axis.vertical,
itemCount: playlist.length,
itemBuilder: (BuildContext context, int index) {
print(playlist.length);
if (index == 0) {
return Container(
height: 60,
padding: EdgeInsets.symmetric(horizontal: 20.0),
alignment: Alignment.topLeft,
height: 300,
width: MediaQuery.of(context).size.width,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
),
child: Column(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
height: 60.0,
// color: context.primaryColorDark,
alignment: Alignment.centerLeft,
child: Row(
children: <Widget>[
Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
height: 20.0,
// color: context.primaryColorDark,
alignment: Alignment.centerLeft,
child: Text(
'Playlist',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
fontSize: 16),
),
),
Spacer(),
Container(
height: 60,
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text(
'Playlist',
style: TextStyle(
color: Theme.of(context).accentColor,
fontWeight: FontWeight.bold,
fontSize: 16),
),
Spacer(),
Container(
alignment: Alignment.center,
child: Container(
alignment: Alignment.center,
height: 40,
width: 80,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius:
BorderRadius.all(Radius.circular(20)),
border: Border.all(
color: Theme.of(context).brightness ==
Brightness.dark
? Colors.black12
: Colors.white10,
width: 1),
boxShadow: Theme.of(context).brightness ==
Brightness.dark
? _customShadowNight
: _customShadow),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(20)),
onTap: () => audio.playNext(),
child: SizedBox(
height: 40,
width: 80,
child: Icon(
Icons.skip_next,
size: 30,
),
),
),
),
),
),
],
),
);
} else {
return Column(
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(playlist[index]);
},
child: Container(
height: 60,
padding: EdgeInsets.symmetric(horizontal: 10),
alignment: Alignment.centerLeft,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius:
BorderRadius.all(Radius.circular(15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${playlist[index].imagePath}"))),
),
),
Expanded(
child: Container(
alignment: Alignment.centerLeft,
child: Text(
playlist[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
],
),
height: 30,
width: 60,
decoration: BoxDecoration(
color: Theme.of(context).primaryColor,
borderRadius: BorderRadius.all(Radius.circular(15)),
border: Border.all(
color:
Theme.of(context).brightness == Brightness.dark
? Colors.black12
: Colors.white10,
width: 1),
boxShadow:
Theme.of(context).brightness == Brightness.dark
? _customShadowNight
: _customShadow),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(15)),
onTap: () {
audio.playNext();
_miniPlaylistKey.currentState.removeItem(
0, (context, animation) => Container());
_miniPlaylistKey.currentState.removeItem(
1, (context, animation) => Container());
_miniPlaylistKey.currentState.insertItem(0);
},
child: SizedBox(
height: 30,
width: 60,
child: Icon(
Icons.skip_next,
size: 30,
),
),
),
Divider(height: 2),
],
);
}
),
),
),
Container(
margin: EdgeInsets.only(left: 20),
width: 30.0,
height: 30.0,
decoration: BoxDecoration(
boxShadow: (Theme.of(context).brightness == Brightness.dark)
? _customShadowNight
: _customShadow,
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(Radius.circular(15.0)),
onTap: () {
Navigator.push(
context,
SlideLeftRoute(page: PlaylistPage()),
);
},
child: SizedBox(
height: 30.0,
width: 30.0,
child: Transform.rotate(
angle: math.pi,
child: Icon(
LineIcons.database_solid,
size: 20.0,
),
),
),
),
),
),
],
),
),
Expanded(
child: Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist,
builder: (_, playlist, __) {
return AnimatedList(
key: _miniPlaylistKey,
shrinkWrap: true,
scrollDirection: Axis.vertical,
initialItemCount: playlist.length,
itemBuilder: (context, index, animation) => ScaleTransition(
alignment: Alignment.centerLeft,
scale: animation,
child: index == 0
? Center()
: Column(
children: <Widget>[
Row(
children: <Widget>[
Expanded(
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
audio.episodeLoad(playlist[index]);
_miniPlaylistKey.currentState
.removeItem(
index,
(context, animation) =>
Center());
_miniPlaylistKey.currentState
.insertItem(0);
},
child: Container(
height: 60,
padding: EdgeInsets.symmetric(
horizontal: 20),
alignment: Alignment.centerLeft,
child: Row(
mainAxisAlignment:
MainAxisAlignment.center,
crossAxisAlignment:
CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Container(
padding: EdgeInsets.all(10.0),
child: ClipRRect(
borderRadius:
BorderRadius.all(
Radius.circular(
15.0)),
child: Container(
height: 30.0,
width: 30.0,
child: Image.file(File(
"${playlist[index].imagePath}"))),
),
),
Expanded(
child: Container(
alignment:
Alignment.centerLeft,
child: Text(
playlist[index].title,
maxLines: 1,
overflow:
TextOverflow.ellipsis,
),
),
),
],
),
),
),
),
),
Container(
margin:
EdgeInsets.symmetric(horizontal: 20),
width: 30.0,
height: 30.0,
decoration: BoxDecoration(
boxShadow:
(Theme.of(context).brightness ==
Brightness.dark)
? _customShadowNight
: _customShadow,
color: Theme.of(context).primaryColor,
shape: BoxShape.circle,
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius: BorderRadius.all(
Radius.circular(15.0)),
onTap: () async {
await audio
.moveToTop(playlist[index]);
_miniPlaylistKey.currentState
.removeItem(
index,
(context, animation) => Center(),
duration:
Duration(milliseconds: 500),
);
_miniPlaylistKey.currentState
.insertItem(1,
duration: Duration(
milliseconds: 200));
},
child: SizedBox(
height: 30.0,
width: 30.0,
child: Transform.rotate(
angle: math.pi,
child: Icon(
LineIcons.download_solid,
size: 20.0,
),
),
),
),
),
),
],
),
Divider(height: 2),
],
),
),
);
},
);
},
),
),
),
],
),
);
}
@ -765,8 +883,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
tabs: <Widget>[
Container(
// child: Text('p'),
height: 10.0,
width: 10.0,
height: 8.0,
width: 8.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
@ -775,8 +893,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
width: 2.0)),
),
Container(
height: 10.0,
width: 10.0,
height: 8.0,
width: 8.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
@ -785,8 +903,8 @@ class _PlayerWidgetState extends State<PlayerWidget> {
width: 2.0)),
),
Container(
height: 10.0,
width: 10.0,
height: 8.0,
width: 8.0,
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.transparent,
@ -971,6 +1089,83 @@ class _PlayerWidgetState extends State<PlayerWidget> {
}
}
class LastPosition extends StatefulWidget {
LastPosition({Key key}) : super(key: key);
@override
_LastPositionState createState() => _LastPositionState();
}
class _LastPositionState extends State<LastPosition> {
static String _stringForSeconds(double seconds) {
if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
}
Future<PlayHistory> getPosition(EpisodeBrief episode) async {
var dbHelper = DBHelper();
return await dbHelper.getPosition(episode);
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return Selector<AudioPlayerNotifier, EpisodeBrief>(
selector: (_, audio) => audio.episode,
builder: (context, episode, child) {
return FutureBuilder<PlayHistory>(
future: getPosition(episode),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return snapshot.hasData
? snapshot.data.seekValue > 0.95
? Container(
height: 20.0,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Theme.of(context)
.textTheme
.bodyText1
.color),
borderRadius:
BorderRadius.all(Radius.circular(10.0))),
child: Text('Played before'))
: snapshot.data.seconds < 10
? Center()
: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.all(Radius.circular(10.0)),
onTap: () => audio.seekTo(
(snapshot.data.seconds * 1000).toInt()),
child: Container(
width: 120.0,
height: 20.0,
alignment: Alignment.center,
decoration: BoxDecoration(
border: Border.all(
width: 1,
color: Theme.of(context)
.textTheme
.bodyText1
.color),
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
child: Text('Last time ' +
_stringForSeconds(snapshot.data.seconds)),
),
),
)
: Center();
});
},
);
}
}
class ImageRotate extends StatefulWidget {
final String title;
final String path;

133
lib/home/download_list.dart Normal file
View File

@ -0,0 +1,133 @@
import 'dart:io';
import 'package:provider/provider.dart';
import 'package:flutter/material.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/episodes/episodedetail.dart';
import 'package:tsacdop/util/pageroute.dart';
class DownloadList extends StatefulWidget {
DownloadList({Key key}) : super(key: key);
@override
_DownloadListState createState() => _DownloadListState();
}
Widget _downloadButton(EpisodeTask task, BuildContext context) {
var downloader = Provider.of<DownloadState>(context, listen: false);
switch (task.status.value) {
case 2:
return IconButton(
icon: Icon(
Icons.pause_circle_filled,
),
onPressed: () => downloader.pauseTask(task.episode),
);
case 4:
return Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
IconButton(
icon: Icon(Icons.refresh),
onPressed: () => downloader.retryTask(task.episode),
),
IconButton(
icon: Icon(Icons.close),
onPressed: () => downloader.delTask(task.episode),
),
],
);
case 6:
return IconButton(
icon: Icon(Icons.play_circle_filled),
onPressed: () => downloader.resumeTask(task.episode),
);
break;
default:
return Center();
}
}
class _DownloadListState extends State<DownloadList> {
@override
Widget build(BuildContext context) {
return SliverPadding(
padding: EdgeInsets.all(5.0),
sliver: Consumer<DownloadState>(builder: (_, downloader, __) {
var tasks = downloader.episodeTasks
.where((task) => task.status.value != 3)
.toList();
return tasks.length > 0
? SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return ListTile(
onTap: () => Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: tasks[index].episode,
)),
),
title: Row(
mainAxisSize: MainAxisSize.min,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Expanded(
flex: 5,
child: Container(
child: Text(
tasks[index].episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
),
Expanded(
flex: 1,
child: tasks[index].progress >= 0
? Container(
width: 40.0,
padding:
EdgeInsets.symmetric(horizontal: 2),
alignment: Alignment.center,
decoration: BoxDecoration(
borderRadius: BorderRadius.all(
Radius.circular(6)),
color: Colors.red),
child: Text(
tasks[index].progress.toString() + '%',
style: TextStyle(color: Colors.white),
))
: Container(),
),
],
),
subtitle: SizedBox(
height: 2,
child: LinearProgressIndicator(
value: tasks[index].progress / 100,
),
),
leading: CircleAvatar(
backgroundImage: FileImage(
File("${tasks[index].episode.imagePath}")),
),
trailing: _downloadButton(tasks[index], context),
);
},
childCount: tasks.length,
),
)
: SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Center();
},
childCount: 1,
),
);
}),
);
}
}

View File

@ -347,7 +347,7 @@ class _PodcastPreviewState extends State<PodcastPreview> {
}
return (snapshot.hasData)
? ShowEpisode(
podcast: snapshot.data,
episodes: snapshot.data,
podcastLocal: widget.podcastLocal,
)
: Center(child: CircularProgressIndicator());
@ -400,9 +400,9 @@ class _PodcastPreviewState extends State<PodcastPreview> {
}
class ShowEpisode extends StatelessWidget {
final List<EpisodeBrief> podcast;
final List<EpisodeBrief> episodes;
final PodcastLocal podcastLocal;
ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key);
ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key);
@override
Widget build(BuildContext context) {
@ -513,16 +513,16 @@ class ShowEpisode extends StatelessWidget {
details.globalPosition.dy),
onLongPress: () => _showPopupMenu(
offset,
podcast[index],
episodes[index],
context,
data.item1 == podcast[index],
data.item2.contains(podcast[index].enclosureUrl)),
data.item1 == episodes[index],
data.item2.contains(episodes[index].enclosureUrl)),
onTap: () {
Navigator.push(
context,
ScaleRoute(
page: EpisodeDetail(
episodeItem: podcast[index],
episodeItem: episodes[index],
heroTag: 'scroll',
//unique hero tag
)),
@ -548,7 +548,7 @@ class ShowEpisode extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Hero(
tag: podcast[index].enclosureUrl +
tag: episodes[index].enclosureUrl +
'scroll',
child: Container(
height: _width / 18,
@ -560,7 +560,7 @@ class ShowEpisode extends StatelessWidget {
),
),
Spacer(),
index < podcastLocal.upateCount
episodes[index].isNew == 1
? Text(
'New',
style: TextStyle(
@ -577,7 +577,7 @@ class ShowEpisode extends StatelessWidget {
padding: EdgeInsets.only(top: 2.0),
alignment: Alignment.topLeft,
child: Text(
podcast[index].title,
episodes[index].title,
style: TextStyle(
fontSize: _width / 32,
),
@ -591,7 +591,7 @@ class ShowEpisode extends StatelessWidget {
child: Container(
alignment: Alignment.bottomLeft,
child: Text(
podcast[index].dateToString(),
episodes[index].dateToString(),
//podcast[index].pubDate.substring(4, 16),
style: TextStyle(
fontSize: _width / 35,
@ -608,7 +608,7 @@ class ShowEpisode extends StatelessWidget {
),
);
},
childCount: (podcast.length > 3) ? 3 : podcast.length,
childCount: (episodes.length > 3) ? 3 : episodes.length,
),
),
),

View File

@ -1,13 +1,15 @@
import 'dart:async';
import 'dart:io';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart' hide NestedScrollView;
import 'package:provider/provider.dart';
import 'package:tsacdop/class/download_state.dart';
import 'package:tsacdop/home/playlist.dart';
import 'package:tuple/tuple.dart';
import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import 'package:tsacdop/util/episodegrid.dart';
import 'package:tsacdop/util/mypopupmenu.dart';
@ -15,6 +17,7 @@ import 'package:tsacdop/util/mypopupmenu.dart';
import 'package:tsacdop/home/appbar/importompl.dart';
import 'package:tsacdop/home/audioplayer.dart';
import 'home_groups.dart';
import 'download_list.dart';
class Home extends StatefulWidget {
@override
@ -58,6 +61,10 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
Import(),
Expanded(
child: NestedScrollView(
innerScrollPositionKeyBuilder: () {
return Key('tab' + _controller.index.toString());
},
pinnedHeaderSliverHeightBuilder: () => 50,
headerSliverBuilder:
(BuildContext context, bool innerBoxScrolled) {
return <Widget>[
@ -100,9 +107,12 @@ class _HomeState extends State<Home> with SingleTickerProviderStateMixin {
body: TabBarView(
controller: _controller,
children: <Widget>[
_RecentUpdate(),
_MyFavorite(),
_MyDownload(),
NestedScrollViewInnerScrollPositionKeyWidget(
Key('tab0'), _RecentUpdate()),
NestedScrollViewInnerScrollPositionKeyWidget(
Key('tab1'), _MyFavorite()),
NestedScrollViewInnerScrollPositionKeyWidget(
Key('tab2'), _MyDownload()),
],
),
),
@ -317,13 +327,11 @@ class _RecentUpdate extends StatefulWidget {
_RecentUpdateState createState() => _RecentUpdateState();
}
class _RecentUpdateState extends State<_RecentUpdate> {
int _updateCount = 0;
class _RecentUpdateState extends State<_RecentUpdate>
with AutomaticKeepAliveClientMixin {
Future<List<EpisodeBrief>> _getRssItem(int top) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRecentRssItem(top);
KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');
_updateCount = await refreshcountstorage.getInt();
return episodes;
}
@ -337,18 +345,18 @@ class _RecentUpdateState extends State<_RecentUpdate> {
});
}
int _top;
int _top = 99;
bool _loadMore;
@override
void initState() {
super.initState();
_loadMore = false;
_top = 33;
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder<List<EpisodeBrief>>(
future: _getRssItem(_top),
builder: (context, snapshot) {
@ -367,7 +375,6 @@ class _RecentUpdateState extends State<_RecentUpdate> {
slivers: <Widget>[
EpisodeGrid(
episodes: snapshot.data,
updateCount: _updateCount,
),
SliverList(
delegate: SliverChildBuilderDelegate(
@ -386,6 +393,9 @@ class _RecentUpdateState extends State<_RecentUpdate> {
},
);
}
@override
bool get wantKeepAlive => true;
}
class _MyFavorite extends StatefulWidget {
@ -393,7 +403,8 @@ class _MyFavorite extends StatefulWidget {
_MyFavoriteState createState() => _MyFavoriteState();
}
class _MyFavoriteState extends State<_MyFavorite> {
class _MyFavoriteState extends State<_MyFavorite>
with AutomaticKeepAliveClientMixin {
Future<List<EpisodeBrief>> _getLikedRssItem(_top) async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getLikedRssItem(_top);
@ -410,18 +421,18 @@ class _MyFavoriteState extends State<_MyFavorite> {
});
}
int _top;
int _top = 99;
bool _loadMore;
@override
void initState() {
super.initState();
_loadMore = false;
_top = 33;
}
@override
Widget build(BuildContext context) {
super.build(context);
return FutureBuilder<List<EpisodeBrief>>(
future: _getLikedRssItem(_top),
builder: (context, snapshot) {
@ -436,7 +447,6 @@ class _MyFavoriteState extends State<_MyFavorite> {
},
child: CustomScrollView(
key: PageStorageKey<String>('favorite'),
physics: const AlwaysScrollableScrollPhysics(),
slivers: <Widget>[
EpisodeGrid(
episodes: snapshot.data,
@ -459,6 +469,9 @@ class _MyFavoriteState extends State<_MyFavorite> {
},
);
}
@override
bool get wantKeepAlive => true;
}
class _MyDownload extends StatefulWidget {
@ -466,32 +479,45 @@ class _MyDownload extends StatefulWidget {
_MyDownloadState createState() => _MyDownloadState();
}
class _MyDownloadState extends State<_MyDownload> {
Future<List<EpisodeBrief>> _getDownloadedRssItem() async {
var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getDownloadedRssItem();
return episodes;
class _MyDownloadState extends State<_MyDownload>
with AutomaticKeepAliveClientMixin {
@override
Widget build(BuildContext context) {
super.build(context);
return CustomScrollView(
key: PageStorageKey<String>('downloas_list'),
slivers: <Widget>[
DownloadList(),
SliverList(
delegate: SliverChildBuilderDelegate(
(BuildContext context, int index) {
return Container(
alignment: Alignment.centerLeft,
padding: EdgeInsets.all(15.0),
child: Text('Downloaded'),
);
},
childCount: 1,
),
),
Consumer<DownloadState>(
builder: (_, downloader, __) {
var episodes = downloader.episodeTasks
.where((task) => task.status.value == 3)
.toList()
.map((e) => e.episode)
.toList()
.reversed
.toList();
return EpisodeGrid(
episodes: episodes,
);
},
),
],
);
}
@override
Widget build(BuildContext context) {
return FutureBuilder<List<EpisodeBrief>>(
future: _getDownloadedRssItem(),
builder: (context, snapshot) {
if (snapshot.hasError) print(snapshot.error);
return (snapshot.hasData)
? CustomScrollView(
physics: const AlwaysScrollableScrollPhysics(),
primary: false,
slivers: <Widget>[
EpisodeGrid(
episodes: snapshot.data,
showDownload: true,
)
],
)
: Center(child: CircularProgressIndicator());
},
);
}
bool get wantKeepAlive => true;
}

View File

@ -12,6 +12,7 @@ import 'package:line_icons/line_icons.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/episodebrief.dart';
import 'package:tsacdop/util/colorize.dart';
import 'package:tsacdop/util/context_extension.dart';
class PlaylistPage extends StatefulWidget {
@override
@ -19,21 +20,8 @@ class PlaylistPage extends StatefulWidget {
}
class _PlaylistPageState extends State<PlaylistPage> {
final GlobalKey<AnimatedListState> _playlistKey = GlobalKey();
final textstyle = TextStyle(fontSize: 15.0, color: Colors.black);
Widget _episodeTag(String text, Color color) {
return Container(
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 23.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.center,
child: Text(text, style: TextStyle(fontSize: 14.0, color: Colors.black)),
);
}
int _sumPlaylistLength(List<EpisodeBrief> episodes) {
int sum = 0;
if (episodes.length == 0) {
@ -87,13 +75,16 @@ class _PlaylistPageState extends State<PlaylistPage> {
),
body: SafeArea(
child:
Selector<AudioPlayerNotifier, Tuple2<List<EpisodeBrief>, bool>>(
Selector<AudioPlayerNotifier, Tuple3<Playlist, bool, bool>>(
selector: (_, audio) =>
Tuple2(audio.queue.playlist, audio.playerRunning),
Tuple3(audio.queue, audio.playerRunning, audio.queueUpdate),
builder: (_, data, __) {
print('update');
final List<EpisodeBrief> episodes = data.item1.playlist;
return Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisAlignment: MainAxisAlignment.start,
children: <Widget>[
Container(
height: _topHeight,
@ -115,7 +106,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
),
children: <TextSpan>[
TextSpan(
text: data.item1.length.toString(),
text: episodes.length.toString(),
style: GoogleFonts.cairo(
textStyle: TextStyle(
color: Theme.of(context).accentColor,
@ -124,7 +115,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
),
),
TextSpan(
text: data.item1.length < 2
text: episodes.length < 2
? ' episode '
: ' episodes ',
style: TextStyle(
@ -133,7 +124,7 @@ class _PlaylistPageState extends State<PlaylistPage> {
)),
TextSpan(
text:
_sumPlaylistLength(data.item1).toString(),
_sumPlaylistLength(episodes).toString(),
style: GoogleFonts.cairo(
textStyle: TextStyle(
color: Theme.of(context).accentColor,
@ -179,171 +170,24 @@ class _PlaylistPageState extends State<PlaylistPage> {
height: 3,
),
Expanded(
child: AnimatedList(
controller: _controller,
key: _playlistKey,
shrinkWrap: true,
child: ReorderableListView(
onReorder: (int oldIndex, int newIndex) {
if (newIndex > oldIndex) {
newIndex -= 1;
}
final EpisodeBrief episodeRemove =
episodes[oldIndex];
audio.delFromPlaylist(episodeRemove);
audio.addToPlaylistAt(episodeRemove, newIndex);
setState(() {});
},
scrollDirection: Axis.vertical,
initialItemCount: data.item1.length,
itemBuilder: (context, index, animation) {
Color _c = (Theme.of(context).brightness ==
Brightness.light)
? data.item1[index].primaryColor.colorizedark()
: data.item1[index].primaryColor.colorizeLight();
return ScaleTransition(
alignment: Alignment.centerLeft,
scale: animation,
child: Dismissible(
background: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment:
MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red),
padding: EdgeInsets.all(5),
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle,
color: Colors.red),
padding: EdgeInsets.all(5),
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
],
),
height: 50,
color: Theme.of(context).accentColor,
),
key: Key(data.item1[index].enclosureUrl),
onDismissed: (direction) async {
await audio.delFromPlaylist(data.item1[index]);
_playlistKey.currentState.removeItem(
index, (context, animation) => Center());
Fluttertoast.showToast(
msg: 'Removed From Playlist',
gravity: ToastGravity.BOTTOM,
);
},
child: Column(
children: <Widget>[
ListTile(
title: Container(
padding: EdgeInsets.only(
top: 10.0, bottom: 5.0),
child: Text(
data.item1[index].title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
leading: CircleAvatar(
backgroundColor: _c.withOpacity(0.5),
backgroundImage: FileImage(File(
"${data.item1[index].imagePath}")),
),
trailing: index == 0
? data.item2
? Padding(
padding: EdgeInsets.only(
right: 15, top: 20),
child: SizedBox(
width: 20,
height: 15,
child: WaveLoader()),
)
: IconButton(
icon: Icon(
Icons.play_arrow,
color: Theme.of(context)
.accentColor,
),
onPressed: () =>
audio.playlistLoad())
: Transform.rotate(
angle: math.pi,
child: IconButton(
tooltip: 'Move to Top',
icon: Icon(
LineIcons.download_solid),
onPressed: () async {
await audio.moveToTop(
data.item1[index]);
_playlistKey.currentState
.removeItem(
index,
(context,
animation) =>
Container());
data.item2
? _playlistKey
.currentState
.insertItem(1)
: _playlistKey
.currentState
.insertItem(0);
}),
),
subtitle: Container(
padding:
EdgeInsets.only(top: 5, bottom: 10),
child: Row(
children: <Widget>[
(data.item1[index].explicit == 1)
? Container(
decoration: BoxDecoration(
color: Colors.red[800],
shape: BoxShape.circle),
height: 20.0,
width: 20.0,
margin: EdgeInsets.only(
right: 10.0),
alignment: Alignment.center,
child: Text('E',
style: TextStyle(
color: Colors.white)))
: Center(),
data.item1[index].duration != 0
? _episodeTag(
(data.item1[index].duration)
.toString() +
'mins',
Colors.cyan[300])
: Center(),
data.item1[index].enclosureLength !=
null
? _episodeTag(
((data.item1[index]
.enclosureLength) ~/
1000000)
.toString() +
'MB',
Colors.lightBlue[300])
: Center(),
],
),
),
),
Divider(
height: 2,
),
],
),
),
);
}),
children: episodes
.map<Widget>((episode) => DismissibleContainer(
episode: episode,
key: ValueKey(episode.enclosureUrl),
))
.toList()),
),
],
);
@ -354,3 +198,152 @@ class _PlaylistPageState extends State<PlaylistPage> {
);
}
}
class DismissibleContainer extends StatefulWidget {
final EpisodeBrief episode;
DismissibleContainer({this.episode, Key key}) : super(key: key);
@override
_DismissibleContainerState createState() => _DismissibleContainerState();
}
class _DismissibleContainerState extends State<DismissibleContainer> {
bool _delete;
Widget _episodeTag(String text, Color color) {
return Container(
decoration: BoxDecoration(
color: color, borderRadius: BorderRadius.all(Radius.circular(15.0))),
height: 23.0,
margin: EdgeInsets.only(right: 10.0),
padding: EdgeInsets.symmetric(horizontal: 8.0),
alignment: Alignment.center,
child: Text(text, style: TextStyle(fontSize: 14.0, color: Colors.black)),
);
}
@override
void initState() {
_delete = false;
super.initState();
}
@override
Widget build(BuildContext context) {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
return AnimatedContainer(
duration: Duration(milliseconds: 300),
height: _delete ? 0 : 95.0,
child: _delete
? Container(
color: context.accentColor,
)
: Dismissible(
key: ValueKey(widget.episode.enclosureUrl + 't'),
background: Container(
padding: EdgeInsets.symmetric(horizontal: 20.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: <Widget>[
Container(
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.red),
padding: EdgeInsets.all(5),
alignment: Alignment.center,
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
Container(
decoration: BoxDecoration(
shape: BoxShape.circle, color: Colors.red),
padding: EdgeInsets.all(5),
alignment: Alignment.center,
child: Icon(
LineIcons.trash_alt_solid,
color: Colors.white,
size: 15,
),
),
],
),
height: 50,
color: Theme.of(context).accentColor,
),
onDismissed: (direction) async {
setState(() {
_delete = true;
});
int index = await audio.delFromPlaylist(widget.episode);
final episodeRemove = widget.episode;
Fluttertoast.showToast(
msg: 'Removed From Playlist',
gravity: ToastGravity.BOTTOM,
);
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('1 episode removed'),
action: SnackBarAction(
label: 'Undo',
onPressed: () {
audio.addToPlaylistAt(episodeRemove, index);
}),
));
},
child: Column(
children: <Widget>[
ListTile(
title: Container(
padding: EdgeInsets.only(top: 10.0, bottom: 5.0),
child: Text(
widget.episode.title,
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
leading: CircleAvatar(
//backgroundColor: _c.withOpacity(0.5),
backgroundImage:
FileImage(File("${widget.episode.imagePath}")),
),
subtitle: Container(
padding: EdgeInsets.only(top: 5, bottom: 10),
child: Row(
children: <Widget>[
(widget.episode.explicit == 1)
? Container(
decoration: BoxDecoration(
color: Colors.red[800],
shape: BoxShape.circle),
height: 20.0,
width: 20.0,
margin: EdgeInsets.only(right: 10.0),
alignment: Alignment.center,
child: Text('E',
style: TextStyle(color: Colors.white)))
: Center(),
widget.episode.duration != 0
? _episodeTag(
(widget.episode.duration).toString() + 'mins',
Colors.cyan[300])
: Center(),
widget.episode.enclosureLength != null
? _episodeTag(
((widget.episode.enclosureLength) ~/ 1000000)
.toString() +
'MB',
Colors.lightBlue[300])
: Center(),
],
),
),
),
// Divider(
// height: 2,
// ),
],
),
),
);
}
}

View File

@ -28,9 +28,10 @@ class DBHelper {
void _onCreate(Database db, int version) async {
await db
.execute("""CREATE TABLE PodcastLocal(id TEXT PRIMARY KEY,title TEXT,
imageUrl TEXT,rssUrl TEXT UNIQUE,primaryColor TEXT,author TEXT,
imageUrl TEXT,rssUrl TEXT UNIQUE, primaryColor TEXT, author TEXT,
description TEXT, add_date INTEGER, imagePath TEXT, provider TEXT, link TEXT,
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0)""");
background_image TEXT DEFAULT '',hosts TEXT DEFAULT '',update_count INTEGER DEFAULT 0,
episode_count INTEGER DEFAULT 0)""");
await db
.execute("""CREATE TABLE Episodes(id INTEGER PRIMARY KEY,title TEXT,
enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT,
@ -52,10 +53,9 @@ class DBHelper {
await Future.forEach(podcasts, (s) async {
List<Map> list;
list = await dbClient.rawQuery(
'SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider, link ,update_count FROM PodcastLocal WHERE id = ?',
"""SELECT id, title, imageUrl, rssUrl, primaryColor, author, imagePath , provider,
link ,update_count, episode_count FROM PodcastLocal WHERE id = ?""",
[s]);
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [s]));
podcastLocal.add(PodcastLocal(
list.first['title'],
list.first['imageUrl'],
@ -67,7 +67,7 @@ class DBHelper {
list.first['provider'],
list.first['link'],
upateCount: list.first['update_count'],
episodeCount: count));
episodeCount: list.first['episode_count']));
});
return podcastLocal;
}
@ -94,6 +94,14 @@ class DBHelper {
return podcastLocal;
}
Future<List<int>> getPodcastCounts(String id) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
'SELECT update_count, episode_count FROM PodcastLocal WHERE id = ?',
[id]);
return [list.first['update_count'], list.first['episode_count']];
}
Future<bool> checkPodcast(String url) async {
var dbClient = await database;
List<Map> list = await dbClient
@ -122,8 +130,6 @@ class DBHelper {
podcastLocal.provider,
podcastLocal.link
]);
});
await dbClient.transaction((txn) async {
await txn.rawInsert(
"""REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""",
[
@ -240,12 +246,17 @@ class DBHelper {
return (sum ~/ 60).toDouble();
}
Future<int> getPosition(EpisodeBrief episodeBrief) async {
Future<PlayHistory> getPosition(EpisodeBrief episodeBrief) async {
var dbClient = await database;
List<Map> list = await dbClient.rawQuery(
"SELECT seconds FROM PlayHistory Where enclosure_url = ?",
"SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory Where enclosure_url = ?",
[episodeBrief.enclosureUrl]);
return list.length > 0 ? list.first['seconds'] : 0;
return list.length > 0
? PlayHistory(list.first['title'], list.first['enclosure_url'],
list.first['seconds'], list.first['seek_value'],
playdate:
DateTime.fromMillisecondsSinceEpoch(list.first['add_date']))
: PlayHistory(episodeBrief.title, episodeBrief.enclosureUrl, 0, 0);
}
DateTime _parsePubDate(String pubDate) {
@ -335,13 +346,6 @@ class DBHelper {
print(feed.items[i].title);
description = _getDescription(feed.items[i].content.value ?? '',
feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
// if (feed.items[i].itunes.summary != null) {
// feed.items[i].itunes.summary.contains('<')
// ? description = feed.items[i].itunes.summary
// : description = feed.items[i].description;
// } else {
// description = feed.items[i].description;
// }
if (feed.items[i].enclosure != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
@ -377,6 +381,12 @@ class DBHelper {
});
}
}
int countUpdate = Sqflite.firstIntValue(await dbClient
.rawQuery('SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [id]));
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET episode_count = ? WHERE id = ?""",
[countUpdate, id]);
return result;
}
@ -391,57 +401,56 @@ class DBHelper {
int count = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
print(count);
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET update_count = ? WHERE id = ?""",
[(result - count), podcastLocal.id]);
if (count == result) {
result = 0;
return result;
} else {
for (int i = 0; i < (result - count); i++) {
print(feed.items[i].title);
description = _getDescription(
feed.items[i].content.value ?? '',
feed.items[i].description ?? '',
feed.items[i].itunes.summary ?? '');
"UPDATE Episodes SET is_new = 0 WHERE feed_id = ?", [podcastLocal.id]);
if (feed.items[i].enclosure?.url != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
}
for (int i = 0; i < result; i++) {
print(feed.items[i].title);
description = _getDescription(feed.items[i].content.value ?? '',
feed.items[i].description ?? '', feed.items[i].itunes.summary ?? '');
final title = feed.items[i].itunes.title ?? feed.items[i].title;
final length = feed.items[i]?.enclosure?.length ?? 0;
final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
if (url != null) {
await dbClient.transaction((txn) {
return txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""",
[
title,
url,
length,
pubDate,
description,
podcastLocal.id,
milliseconds,
duration,
explicit,
url
]);
});
}
if (feed.items[i].enclosure?.url != null) {
_isXimalaya(feed.items[i].enclosure.url)
? url = feed.items[i].enclosure.url.split('=').last
: url = feed.items[i].enclosure.url;
}
final title = feed.items[i].itunes.title ?? feed.items[i].title;
final length = feed.items[i]?.enclosure?.length ?? 0;
final pubDate = feed.items[i].pubDate;
final date = _parsePubDate(pubDate);
final milliseconds = date.millisecondsSinceEpoch;
final duration = feed.items[i].itunes.duration?.inMinutes ?? 0;
final explicit = _getExplicit(feed.items[i].itunes.explicit);
if (url != null) {
await dbClient.transaction((txn) async {
int id = await txn.rawInsert(
"""INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate,
description, feed_id, milliseconds, duration, explicit, media_id, is_new) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, 1)""",
[
title,
url,
length,
pubDate,
description,
podcastLocal.id,
milliseconds,
duration,
explicit,
url,
]);
print("$id");
});
}
return (result - count) < 0 ? 0 : (result - count);
}
int countUpdate = Sqflite.firstIntValue(await dbClient.rawQuery(
'SELECT COUNT(*) FROM Episodes WHERE feed_id = ?', [podcastLocal.id]));
await dbClient.rawUpdate(
"""UPDATE PodcastLocal SET update_count = ?, episode_count = ? WHERE id = ?""",
[countUpdate - count, countUpdate, podcastLocal.id]);
return countUpdate - count;
}
Future<List<EpisodeBrief>> getRssItem(String id, int i) async {
@ -450,7 +459,7 @@ class DBHelper {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor , E.media_id
E.downloaded, P.primaryColor , E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]);
for (int x = 0; x < list.length; x++) {
@ -466,7 +475,8 @@ class DBHelper {
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id']));
list[x]['media_id'],
list[x]['is_new']));
}
return episodes;
}
@ -477,7 +487,7 @@ class DBHelper {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor, E.media_id
E.downloaded, P.primaryColor, E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.feed_id = ? ORDER BY E.milliseconds DESC LIMIT 3""", [id]);
for (int x = 0; x < list.length; x++) {
@ -493,7 +503,8 @@ class DBHelper {
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id']));
list[x]['media_id'],
list[x]['is_new']));
}
return episodes;
}
@ -504,7 +515,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.primaryColor, E.media_id
E.downloaded, P.primaryColor, E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
[url]);
@ -522,7 +533,8 @@ class DBHelper {
list.first['duration'],
list.first['explicit'],
list.first['imagePath'],
list.first['media_id']);
list.first['media_id'],
list.first['is_new']);
return episode;
}
@ -532,7 +544,7 @@ class DBHelper {
List<Map> list = await dbClient
.rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length,
E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked,
E.downloaded, P.imagePath, P.primaryColor, E.media_id
E.downloaded, P.imagePath, P.primaryColor, E.media_id, E.is_new
FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
ORDER BY E.milliseconds DESC LIMIT ? """, [top]);
for (int x = 0; x < list.length; x++) {
@ -548,7 +560,8 @@ class DBHelper {
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id']));
list[x]['media_id'],
list[x]['is_new']));
}
return episodes;
}
@ -559,8 +572,8 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""",[i]);
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""", [i]);
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
@ -574,7 +587,8 @@ class DBHelper {
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id']));
list[x]['media_id'],
list[x]['is_new']));
}
return episodes;
}
@ -597,19 +611,19 @@ class DBHelper {
Future<int> saveDownloaded(String url, String id) async {
var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
int milliseconds = DateTime.now().millisecondsSinceEpoch;
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET downloaded = ?, download_date = ? WHERE enclosure_url = ?",
[id, _milliseconds, url]);
[id, milliseconds, url]);
return count;
}
Future<int> saveMediaId(String url, String path) async {
Future<int> saveMediaId(String url, String path, String id) async {
var dbClient = await database;
int _milliseconds = DateTime.now().millisecondsSinceEpoch;
int milliseconds = DateTime.now().millisecondsSinceEpoch;
int count = await dbClient.rawUpdate(
"UPDATE Episodes SET media_id = ?, download_date = ? WHERE enclosure_url = ?",
[path, _milliseconds, url]);
"UPDATE Episodes SET media_id = ?, download_date = ?, downloaded = ? WHERE enclosure_url = ?",
[path, milliseconds, id, url]);
return count;
}
@ -628,8 +642,8 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC LIMIT 99""");
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.downloaded != 'ND' ORDER BY E.download_date DESC""");
for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief(
list[x]['title'],
@ -643,7 +657,8 @@ class DBHelper {
list[x]['duration'],
list[x]['explicit'],
list[x]['imagePath'],
list[x]['media_id']));
list[x]['media_id'],
list[x]['is_new']));
}
return episodes;
}
@ -670,7 +685,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.enclosure_url = ?""", [url]);
if (list.length == 0) {
return null;
@ -687,7 +702,8 @@ class DBHelper {
list.first['duration'],
list.first['explicit'],
list.first['imagePath'],
list.first['media_id']);
list.first['media_id'],
list.first['is_new']);
return episode;
}
}
@ -698,7 +714,7 @@ class DBHelper {
List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath,
P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded,
P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
WHERE E.media_id = ?""", [id]);
episode = EpisodeBrief(
list.first['title'],
@ -712,7 +728,8 @@ class DBHelper {
list.first['duration'],
list.first['explicit'],
list.first['imagePath'],
list.first['media_id']);
list.first['media_id'],
list.first['is_new']);
return episode;
}
}

View File

@ -8,12 +8,13 @@ import 'package:tsacdop/home/appbar/addpodcast.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/importompl.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/class/download_state.dart';
final SettingState themeSetting = SettingState();
Future main() async {
WidgetsFlutterBinding.ensureInitialized();
await themeSetting.initData();
await FlutterDownloader.initialize();
runApp(
MultiProvider(
providers: [
@ -21,13 +22,16 @@ Future main() async {
ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()),
ChangeNotifierProvider(create: (_) => GroupList()),
ChangeNotifierProvider(create: (_) => ImportOmpl()),
ChangeNotifierProvider(create: (_) => DownloadState(),
)
],
child: MyApp(),
),
);
SystemUiOverlayStyle systemUiOverlayStyle =
SystemUiOverlayStyle(statusBarColor: Colors.transparent, systemNavigationBarColor: Colors.transparent);
await FlutterDownloader.initialize();
SystemUiOverlayStyle systemUiOverlayStyle = SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
systemNavigationBarColor: Colors.transparent);
SystemChrome.setSystemUIOverlayStyle(systemUiOverlayStyle);
await SystemChrome.setPreferredOrientations(
@ -67,7 +71,7 @@ class MyApp extends StatelessWidget {
darkTheme: ThemeData.dark().copyWith(
accentColor: setting.accentSetColor,
primaryColorDark: Colors.grey[800],
// scaffoldBackgroundColor: Colors.black87,
// scaffoldBackgroundColor: Colors.black87,
appBarTheme: AppBarTheme(elevation: 0),
),
home: MyHomePage(),

View File

@ -6,6 +6,7 @@ import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:html/parser.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/class/podcast_group.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_linkify/flutter_linkify.dart';
@ -35,15 +36,18 @@ class _PodcastDetailState extends State<PodcastDetail> {
Future _updateRssItem(PodcastLocal podcastLocal) async {
var dbHelper = DBHelper();
final result = await dbHelper.updatePodcastRss(podcastLocal);
result == 0
? Fluttertoast.showToast(
if(result == 0)
{ Fluttertoast.showToast(
msg: 'No Update',
gravity: ToastGravity.TOP,
)
: Fluttertoast.showToast(
);}
else{
Fluttertoast.showToast(
msg: 'Updated $result Episodes',
gravity: ToastGravity.TOP,
);
Provider.of<GroupList>(context, listen: false).updatePodcast(podcastLocal);
}
if (mounted) setState(() {});
}
@ -400,8 +404,6 @@ class _PodcastDetailState extends State<PodcastDetail> {
episodes: snapshot.data,
showFavorite: true,
showNumber: true,
updateCount:
widget.podcastLocal.upateCount,
episodeCount:
widget.podcastLocal.episodeCount,
),

View File

@ -40,7 +40,7 @@ class _PodcastGroupListState extends State<PodcastGroupList> {
widget.group.podcasts.insert(newIndex, podcast);
});
widget.group.setOrderedPodcasts = widget.group.podcasts;
groupList.addToOrderChanged(widget.group.name);
groupList.addToOrderChanged(widget.group);
},
children: widget.group.podcasts
.map<Widget>((PodcastLocal podcastLocal) {
@ -319,7 +319,7 @@ class _PodcastCardState extends State<PodcastCard> {
bottom: 20),
title: Text('Remove confirm'),
content: Text(
'Are you sure you want to unsubscribe?'),
'Are you sure you want to unsubscribe?'),
actions: <Widget>[
FlatButton(
onPressed: () =>

View File

@ -72,7 +72,7 @@ class _PodcastManageState extends State<PodcastManage>
Widget _saveButton(BuildContext context) {
return Consumer<GroupList>(
builder: (_, groupList, __) {
if (groupList.orderChanged.contains(groupList.groups[_index].name)) {
if (groupList.orderChanged.contains(groupList.groups[_index])) {
_controller.forward();
} else if (_fraction > 0) {
_controller.reverse();
@ -92,8 +92,8 @@ class _PodcastManageState extends State<PodcastManage>
shape: BoxShape.circle,
boxShadow: [
BoxShadow(
color: Colors.grey[700],
blurRadius: 5,
color: Colors.grey[700].withOpacity(0.5),
blurRadius: 1,
offset: Offset(1, 1),
),
]),
@ -161,298 +161,304 @@ class _PodcastManageState extends State<PodcastManage>
OrderMenu(),
],
),
body: Consumer<GroupList>(builder: (_, groupList, __) {
bool _isLoading = groupList.isLoading;
List<PodcastGroup> _groups = groupList.groups;
return _isLoading
? Center()
: Stack(
children: <Widget>[
CustomTabView(
itemCount: _groups.length,
tabBuilder: (context, index) => Tab(
child: Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: (_scroll - index).abs() > 1
? Colors.grey[300]
: Colors.grey[300]
.withOpacity((_scroll - index).abs()),
borderRadius:
BorderRadius.all(Radius.circular(15)),
),
child: Text(
_groups[index].name,
)),
),
pageBuilder: (context, index) => Container(
key: ObjectKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
),
_showSetting
? Positioned.fill(
top: 50,
child: GestureDetector(
onTap: () async {
await _menuController.reverse();
setState(() => _showSetting = false);
},
child: Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5 * _menuController.value),
body: WillPopScope(
onWillPop: () async {
await Provider.of<GroupList>(context, listen: false).clearOrderChanged();
return true;
},
child: Consumer<GroupList>(builder: (_, groupList, __) {
bool _isLoading = groupList.isLoading;
List<PodcastGroup> _groups = groupList.groups;
return _isLoading
? Center()
: Stack(
children: <Widget>[
CustomTabView(
itemCount: _groups.length,
tabBuilder: (context, index) => Tab(
child: Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 10.0),
alignment: Alignment.center,
decoration: BoxDecoration(
color: (_scroll - index).abs() > 1
? Colors.grey[300]
: Colors.grey[300]
.withOpacity((_scroll - index).abs()),
borderRadius:
BorderRadius.all(Radius.circular(15)),
),
),
)
: Center(),
Positioned(
right: 30,
bottom: 30,
child: _saveButton(context),
),
_showSetting
? Positioned(
right: 30 * _menuValue,
bottom: 100,
child: Container(
alignment: Alignment.centerRight,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_menuController.reverse();
setState(() => _showSetting = false);
_index == 0
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 300),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
RenameGroup(
group: _groups[_index],
));
},
child: Container(
height: 30.0,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.text_fields,
color: Colors.white,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Edit Name',
style: TextStyle(
color: Colors.white)),
],
child: Text(
_groups[index].name,
)),
),
pageBuilder: (context, index) => Container(
key: ValueKey(_groups[index].name),
child: PodcastGroupList(group: _groups[index])),
onPositionChange: (value) =>
setState(() => _index = value),
onScroll: (value) => setState(() => _scroll = value),
),
_showSetting
? Positioned.fill(
top: 50,
child: GestureDetector(
onTap: () async {
await _menuController.reverse();
setState(() => _showSetting = false);
},
child: Container(
color: Theme.of(context)
.scaffoldBackgroundColor
.withOpacity(0.5 * _menuController.value),
),
),
)
: Center(),
Positioned(
right: 30,
bottom: 30,
child: _saveButton(context),
),
_showSetting
? Positioned(
right: 30 * _menuValue,
bottom: 100,
child: Container(
alignment: Alignment.centerRight,
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.end,
children: <Widget>[
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_menuController.reverse();
setState(() => _showSetting = false);
_index == 0
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 300),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
RenameGroup(
group: _groups[_index],
));
},
child: Container(
height: 30.0,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.text_fields,
color: Colors.white,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Edit Name',
style: TextStyle(
color: Colors.white)),
],
),
),
),
),
),
Padding(
padding:
EdgeInsets.symmetric(vertical: 10.0)),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_menuController.reverse();
setState(() => _showSetting = false);
_index == 0
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 300),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
AnnotatedRegion<
SystemUiOverlayStyle>(
value:
SystemUiOverlayStyle(
statusBarIconBrightness:
Brightness.light,
systemNavigationBarColor:
Theme.of(context)
.brightness ==
Brightness
.light
? Color
.fromRGBO(
113,
113,
113,
1)
: Color
.fromRGBO(
15,
15,
15,
1),
// statusBarColor: Theme.of(
// context)
// .brightness ==
// Brightness.light
// ? Color.fromRGBO(
// 113,
// 113,
// 113,
// 1)
// : Color.fromRGBO(
// 5, 5, 5, 1),
),
child: AlertDialog(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius:
BorderRadius.all(
Radius.circular(
10.0))),
titlePadding:
EdgeInsets.only(
top: 20,
left: 20,
right: 200,
bottom: 20),
title: Text(
'Delete confirm'),
content: Text(
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Padding(
padding: EdgeInsets.symmetric(
vertical: 10.0)),
Material(
color: Colors.transparent,
child: InkWell(
onTap: () {
_menuController.reverse();
setState(() => _showSetting = false);
_index == 0
? Fluttertoast.showToast(
msg:
'Home group is not supported',
gravity: ToastGravity.BOTTOM,
)
: showGeneralDialog(
context: context,
barrierDismissible: true,
barrierLabel:
MaterialLocalizations.of(
context)
.modalBarrierDismissLabel,
barrierColor: Colors.black54,
transitionDuration:
const Duration(
milliseconds: 300),
pageBuilder: (BuildContext
context,
Animation animaiton,
Animation
secondaryAnimation) =>
AnnotatedRegion<
SystemUiOverlayStyle>(
value:
SystemUiOverlayStyle(
statusBarIconBrightness:
Brightness.light,
systemNavigationBarColor:
Theme.of(context)
.brightness ==
Brightness
.light
? Color
.fromRGBO(
113,
113,
113,
1)
: Color
.fromRGBO(
15,
15,
15,
1),
// statusBarColor: Theme.of(
// context)
// .brightness ==
// Brightness.light
// ? Color.fromRGBO(
// 113,
// 113,
// 113,
// 1)
// : Color.fromRGBO(
// 5, 5, 5, 1),
),
child: AlertDialog(
elevation: 1,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius
.all(Radius
.circular(
10.0))),
titlePadding:
EdgeInsets.only(
top: 20,
left: 20,
right: 200,
bottom: 20),
title: Text(
'Delete confirm'),
content: Text(
'Are you sure you want to delete this group? Podcasts will be moved to Home group.'),
actions: <Widget>[
FlatButton(
onPressed: () =>
Navigator.of(
context)
.pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors
.grey[
600]),
),
),
FlatButton(
onPressed: () {
if (_index ==
groupList
.groups
.length -
1) {
setState(() {
_index =
_index -
1;
_scroll = 0;
});
groupList.delGroup(
_groups[
_index +
1]);
} else {
groupList.delGroup(
_groups[
_index]);
}
Navigator.of(
context)
.pop(),
child: Text(
'CANCEL',
style: TextStyle(
color: Colors
.grey[
600]),
),
),
FlatButton(
onPressed: () {
if (_index ==
groupList
.groups
.length -
1) {
setState(() {
_index =
_index -
1;
_scroll = 0;
});
groupList.delGroup(
_groups[
_index +
1]);
} else {
groupList.delGroup(
_groups[
_index]);
}
Navigator.of(
context)
.pop();
},
child: Text(
'CONFIRM',
style: TextStyle(
color: Colors
.red),
),
)
],
),
));
},
child: Container(
height: 30,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.delete_outline,
color: Colors.red,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Delete',
style: TextStyle(
color: Colors.red)),
],
.pop();
},
child: Text(
'CONFIRM',
style: TextStyle(
color: Colors
.red),
),
)
],
),
));
},
child: Container(
height: 30,
decoration: BoxDecoration(
color: Colors.grey[700],
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
padding: EdgeInsets.symmetric(
horizontal: 10),
child: Row(
children: <Widget>[
Icon(
Icons.delete_outline,
color: Colors.red,
size: 15.0,
),
Padding(
padding: EdgeInsets.symmetric(
horizontal: 5.0),
),
Text('Delete',
style: TextStyle(
color: Colors.red)),
],
),
),
),
),
),
],
],
),
),
),
)
: Center(),
],
);
}),
)
: Center(),
],
);
}),
),
),
);
}

View File

@ -13,6 +13,7 @@ import 'package:flutter_file_dialog/flutter_file_dialog.dart';
import 'package:tsacdop/class/audiostate.dart';
import 'package:tsacdop/util/ompl_build.dart';
import 'package:tsacdop/util/context_extension.dart';
import 'theme.dart';
import 'storage.dart';
import 'history.dart';
@ -56,7 +57,7 @@ class Settings extends StatelessWidget {
appBar: AppBar(
title: Text('Settings'),
elevation: 0,
backgroundColor: Theme.of(context).primaryColor,
backgroundColor: context.primaryColor,
),
body: SafeArea(
child: SingleChildScrollView(
@ -81,7 +82,7 @@ class Settings extends StatelessWidget {
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
.copyWith(color: context.accentColor)),
),
ListView(
physics: ClampingScrollPhysics(),

View File

@ -1,11 +1,14 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:app_settings/app_settings.dart';
import 'package:tsacdop/settings/downloads_manage.dart';
import 'package:tsacdop/class/settingstate.dart';
class StorageSetting extends StatelessWidget {
@override
Widget build(BuildContext context) {
var settings = Provider.of<SettingState>(context, listen: false);
return AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Theme.of(context).accentColorBrightness,
@ -24,6 +27,54 @@ class StorageSetting extends StatelessWidget {
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container(
height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft,
child: Text('Network',
style: Theme.of(context)
.textTheme
.bodyText1
.copyWith(color: Theme.of(context).accentColor)),
),
ListView(
physics: const BouncingScrollPhysics(),
shrinkWrap: true,
scrollDirection: Axis.vertical,
children: <Widget>[
ListTile(
onTap: () => Navigator.push(
context,
MaterialPageRoute(
builder: (context) => DownloadsManage())),
contentPadding:
EdgeInsets.only(left: 80.0, right: 25),
title: Text('Ask before using cellular data'),
subtitle: Text(
'Ask to confirem when using cellular data to download episodes.'),
trailing: Selector<SettingState, bool>(
selector: (_, settings) =>
settings.downloadUsingData,
builder: (_, data, __) {
return Switch(
value: data,
onChanged: (value) =>
settings.downloadUsingData = value,
);
},
),
),
Divider(height: 2),
],
),
]),
Column(
mainAxisAlignment: MainAxisAlignment.start,
mainAxisSize: MainAxisSize.min,

View File

@ -71,11 +71,10 @@ class SyncingSetting extends StatelessWidget {
value: data.item1,
onChanged: (boo) async {
settings.autoUpdate = boo;
if (boo) {
if (boo)
settings.setWorkManager(data.item2);
} else {
else
settings.cancelWork();
}
}),
),
Divider(height: 2),
@ -92,7 +91,8 @@ class SyncingSetting extends StatelessWidget {
elevation: 1,
value: data.item2,
onChanged: data.item1
? (value) {
? (value) async {
await settings.cancelWork();
settings.setWorkManager(value);
}
: null,

View File

@ -3,6 +3,7 @@ import 'package:flutter/services.dart';
import 'package:provider/provider.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
import 'package:tsacdop/class/settingstate.dart';
import 'package:tsacdop/util/context_extension.dart';
class ThemeSetting extends StatelessWidget {
@override
@ -61,11 +62,11 @@ class ThemeSetting extends StatelessWidget {
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
// systemNavigationBarColor:
// Theme.of(context).brightness ==
// Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(15, 15, 15, 1),
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
// statusBarColor:
// Theme.of(context).brightness ==
// Brightness.light
@ -138,11 +139,11 @@ class ThemeSetting extends StatelessWidget {
AnnotatedRegion<SystemUiOverlayStyle>(
value: SystemUiOverlayStyle(
statusBarIconBrightness: Brightness.light,
// systemNavigationBarColor:
// Theme.of(context).brightness ==
// Brightness.light
// ? Color.fromRGBO(113, 113, 113, 1)
// : Color.fromRGBO(15, 15, 15, 1),
systemNavigationBarColor:
Theme.of(context).brightness ==
Brightness.light
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
// statusBarColor:
// Theme.of(context).brightness ==
// Brightness.light
@ -155,7 +156,7 @@ class ThemeSetting extends StatelessWidget {
top: 20,
left: 40,
right: 200,
bottom: 20),
bottom: 0),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(
Radius.circular(10.0))),
@ -165,7 +166,7 @@ class ThemeSetting extends StatelessWidget {
onColorChanged: (value) {
settings.setAccentColor = value;
},
pickerColor: Colors.blue,
pickerColor: context.accentColor,
),
),
))),

View File

@ -0,0 +1,11 @@
import 'package:flutter/material.dart';
extension ContextExtension on BuildContext{
Color get primaryColor => Theme.of(this).primaryColor;
Color get accentColor => Theme.of(this).accentColor;
Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor;
Color get primaryColorDark => Theme.of(this).primaryColorDark;
Brightness get brightness => Theme.of(this).brightness;
double get width => MediaQuery.of(this).size.width;
double get height => MediaQuery.of(this).size.width;
}

View File

@ -1,5 +1,6 @@
import 'dart:io';
import 'dart:isolate';
import 'dart:async';
import 'dart:ui';
import 'package:flutter/material.dart';
@ -21,18 +22,15 @@ class EpisodeGrid extends StatelessWidget {
final bool showFavorite;
final bool showDownload;
final bool showNumber;
final int updateCount;
final String heroTag;
final int episodeCount;
EpisodeGrid(
{Key key,
@required this.episodes,
this.heroTag = '',
this.showDownload = false,
this.showFavorite = false,
this.showNumber = false,
this.updateCount = 0,
this.episodeCount = 0})
this.episodeCount = 0
})
: super(key: key);
@override
@ -183,7 +181,7 @@ class EpisodeGrid extends StatelessWidget {
),
),
Spacer(),
index < updateCount
episodes[index].isNew == 1
? Text('New',
style: TextStyle(
color: Colors.red,
@ -197,7 +195,7 @@ class EpisodeGrid extends StatelessWidget {
? Container(
alignment: Alignment.topRight,
child: Text(
(episodeCount- index).toString(),
(episodeCount - index).toString(),
style: GoogleFonts.teko(
textStyle: TextStyle(
fontSize: _width / 24,
@ -240,10 +238,6 @@ class EpisodeGrid extends StatelessWidget {
),
),
Spacer(),
showDownload
? DownloadIcon(
episodeBrief: episodes[index])
: Center(),
Padding(
padding: EdgeInsets.all(1),
),
@ -280,148 +274,6 @@ class EpisodeGrid extends StatelessWidget {
}
}
class DownloadIcon extends StatefulWidget {
final EpisodeBrief episodeBrief;
DownloadIcon({this.episodeBrief, Key key}) : super(key: key);
@override
_DownloadIconState createState() => _DownloadIconState();
}
class _DownloadIconState extends State<DownloadIcon> {
_TaskInfo _task;
bool _isLoading;
ReceivePort _port = ReceivePort();
@override
void initState() {
super.initState();
_bindBackgroundIsolate();
FlutterDownloader.registerCallback(downloadCallback);
_isLoading = true;
_prepare();
}
@override
void dispose() {
_unbindBackgroundIsolate();
super.dispose();
}
void _bindBackgroundIsolate() {
bool isSuccess = IsolateNameServer.registerPortWithName(
_port.sendPort, 'downloader_send_port');
if (!isSuccess) {
_unbindBackgroundIsolate();
_bindBackgroundIsolate();
return;
}
_port.listen((dynamic data) {
print('UI isolate callback: $data');
String id = data[0];
DownloadTaskStatus status = data[1];
int progress = data[2];
if (_task.taskId == id) {
setState(() {
_task.status = status;
_task.progress = progress;
});
}
});
}
void _unbindBackgroundIsolate() {
IsolateNameServer.removePortNameMapping('downloader_send_port');
}
static void downloadCallback(
String id, DownloadTaskStatus status, int progress) {
print('Background callback task in $id status ($status) $progress');
final SendPort send =
IsolateNameServer.lookupPortByName('downloader_send_port');
send.send([id, status, progress]);
}
Future<Null> _prepare() async {
final tasks = await FlutterDownloader.loadTasks();
_task = _TaskInfo(
name: widget.episodeBrief.title,
link: widget.episodeBrief.enclosureUrl);
tasks?.forEach((task) {
if (_task.link == task.url) {
_task.taskId = task.taskId;
_task.status = task.status;
_task.progress = task.progress;
}
});
setState(() {
_isLoading = false;
});
}
@override
Widget build(BuildContext context) {
return _downloadButton(_task);
}
Widget _downloadButton(_TaskInfo task) {
if (_isLoading)
return Center();
else if (task.status == DownloadTaskStatus.running) {
return SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[200],
strokeWidth: 1,
valueColor:
AlwaysStoppedAnimation<Color>(Theme.of(context).accentColor),
value: task.progress / 100,
),
);
} else if (task.status == DownloadTaskStatus.paused) {
return SizedBox(
height: 12,
width: 12,
child: CircularProgressIndicator(
backgroundColor: Colors.grey[200],
strokeWidth: 1,
valueColor: AlwaysStoppedAnimation<Color>(Colors.red),
value: task.progress / 100,
),
);
} else if (task.status == DownloadTaskStatus.complete) {
return IconTheme(
data: IconThemeData(size: 15),
child: Icon(
Icons.done_all,
color: Theme.of(context).accentColor,
),
);
} else if (task.status == DownloadTaskStatus.failed) {
return IconTheme(
data: IconThemeData(size: 15),
child: Icon(Icons.refresh, color: Colors.red),
);
}
return Center();
}
}
class _TaskInfo {
final String name;
final String link;
String taskId;
int progress = 0;
DownloadTaskStatus status = DownloadTaskStatus.undefined;
_TaskInfo({this.name, this.link});
}
class OpenContainerWrapper extends StatelessWidget {
const OpenContainerWrapper({
@ -469,3 +321,5 @@ class OpenContainerWrapper extends StatelessWidget {
);
}
}

View File

@ -53,13 +53,18 @@ dev_dependencies:
app_settings: ^3.0.1
fl_chart: ^0.8.7
audio_service: ^0.6.2
just_audio: ^0.1.3
just_audio:
git:
url: https://github.com/stonega/just_audio.git
rxdart: ^0.23.1
line_icons:
git:
url: https://github.com/galonsos/line_icons.git
flutter_file_dialog: ^0.0.5
flutter_linkify: ^3.1.0
extended_nested_scroll_view: ^0.4.0
connectivity: ^0.4.8+2
# For information on the generic Dart part of this file, see the
# following page: https://www.dartlang.org/tools/pub/pubspec