From a1d004aa43d242a103198d28de6aa662d5609312 Mon Sep 17 00:00:00 2001 From: stonegate Date: Wed, 1 Apr 2020 00:36:20 +0800 Subject: [PATCH] A lot of bug fixed --- README.md | 4 +- .../app/.settings/org.eclipse.jdt.core.prefs | 4 + lib/class/audiostate.dart | 143 +++-- lib/class/download_state.dart | 191 ++++++ lib/class/downloadstate.dart | 18 - lib/class/episodebrief.dart | 4 +- lib/class/podcast_group.dart | 35 +- lib/class/podcastlocal.dart | 4 +- lib/class/settingstate.dart | 59 +- lib/episodes/episodedetail.dart | 3 +- lib/episodes/episodedownload.dart | 417 ++++++------- lib/home/appbar/about.dart | 8 +- lib/home/appbar/addpodcast.dart | 3 +- lib/home/appbar/popupmenu.dart | 2 +- lib/home/audioplayer.dart | 475 ++++++++++----- lib/home/download_list.dart | 133 ++++ lib/home/home_groups.dart | 24 +- lib/home/nested_home.dart | 108 ++-- lib/home/playlist.dart | 357 ++++++----- lib/local_storage/sqflite_localpodcast.dart | 193 +++--- lib/main.dart | 14 +- lib/podcasts/podcastdetail.dart | 14 +- lib/podcasts/podcastgroup.dart | 4 +- lib/podcasts/podcastmanage.dart | 576 +++++++++--------- lib/settings/settting.dart | 5 +- lib/settings/storage.dart | 51 ++ lib/settings/syncing.dart | 8 +- lib/settings/theme.dart | 25 +- lib/util/context_extension.dart | 11 + lib/util/episodegrid.dart | 160 +---- pubspec.yaml | 7 +- 31 files changed, 1787 insertions(+), 1273 deletions(-) create mode 100644 android/app/.settings/org.eclipse.jdt.core.prefs create mode 100644 lib/class/download_state.dart delete mode 100644 lib/class/downloadstate.dart create mode 100644 lib/home/download_list.dart create mode 100644 lib/util/context_extension.dart diff --git a/README.md b/README.md index 9613e66..b42cf4c 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ [![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/) ## About

- +

@@ -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 diff --git a/android/app/.settings/org.eclipse.jdt.core.prefs b/android/app/.settings/org.eclipse.jdt.core.prefs new file mode 100644 index 0000000..35068d9 --- /dev/null +++ b/android/app/.settings/org.eclipse.jdt.core.prefs @@ -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 diff --git a/lib/class/audiostate.dart b/lib/class/audiostate.dart index 4560f4d..3d7a7cf 100644 --- a/lib/class/audiostate.dart +++ b/lib/class/audiostate.dart @@ -59,12 +59,10 @@ class PlayHistory { } } -class Playlist { +class Playlist extends ChangeNotifier { String name; DBHelper dbHelper = DBHelper(); - // list of urls - //List _urls; - //list of episodes + List _playlist; //list of miediaitem @@ -73,14 +71,13 @@ class Playlist { getPlaylist() async { List 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 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 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 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(); diff --git a/lib/class/download_state.dart b/lib/class/download_state.dart new file mode 100644 index 0000000..80f7c48 --- /dev/null +++ b/lib/class/download_state.dart @@ -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 _episodeTasks = []; + List 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); + } +} diff --git a/lib/class/downloadstate.dart b/lib/class/downloadstate.dart deleted file mode 100644 index 8fc79db..0000000 --- a/lib/class/downloadstate.dart +++ /dev/null @@ -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(); - } -} diff --git a/lib/class/episodebrief.dart b/lib/class/episodebrief.dart index 8b2d9ef..dccb2ed 100644 --- a/lib/class/episodebrief.dart +++ b/lib/class/episodebrief.dart @@ -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); diff --git a/lib/class/podcast_group.dart b/lib/class/podcast_group.dart index 7a79db4..7d3fc03 100644 --- a/lib/class/podcast_group.dart +++ b/lib/class/podcast_group.dart @@ -21,7 +21,6 @@ class GroupEntity { static GroupEntity fromJson(Map json) { List 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 _orderedPodcasts; List get ordereddPodcasts => _orderedPodcasts; List get podcasts => _podcasts; + set setOrderedPodcasts(List list) { _orderedPodcasts = list; } @@ -88,18 +88,29 @@ class GroupList extends ChangeNotifier { bool _isLoading = false; bool get isLoading => _isLoading; - List _orderChanged = []; - List get orderChanged => _orderChanged; - void addToOrderChanged(String name) { - _orderChanged.add(name); + List _orderChanged = []; + List 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 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 getPodcastGroup(String id) { List result = []; _groups.forEach((group) { diff --git a/lib/class/podcastlocal.dart b/lib/class/podcastlocal.dart index fff9ff2..7526474 100644 --- a/lib/class/podcastlocal.dart +++ b/lib/class/podcastlocal.dart @@ -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, diff --git a/lib/class/settingstate.dart b/lib/class/settingstate.dart index b0c17c4..57545ce 100644 --- a/lib/class/settingstate.dart +++ b/lib/class/settingstate.dart @@ -10,24 +10,23 @@ void callbackDispatcher() { Workmanager.executeTask((task, inputData) async { var dbHelper = DBHelper(); List 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); } } diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index fd1127f..5d60292 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -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 { () => setUnliked(widget.episodeItem.enclosureUrl)), ], ), - DownloadButton(episodeBrief: widget.episodeItem), + DownloadButton(episode: widget.episodeItem), Selector>( selector: (_, audio) => audio.queue.playlist.map((e) => e.enclosureUrl).toList(), diff --git a/lib/episodes/episodedownload.dart b/lib/episodes/episodedownload.dart index b145681..ccd8efc 100644 --- a/lib/episodes/episodedownload.dart +++ b/lib/episodes/episodedownload.dart @@ -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 { - _TaskInfo _task; - bool _isLoading; bool _permissionReady; - String _localPath; - ReceivePort _port = ReceivePort(); - - Future _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(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(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(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(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(context, listen: false).retryTask(episode); Fluttertoast.showToast( msg: 'Download again', gravity: ToastGravity.BOTTOM, ); } - Future _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 _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 _checkPermmison() async { PermissionStatus permission = await PermissionHandler() .checkPermissionStatus(PermissionGroup.storage); @@ -205,6 +107,60 @@ class _DownloadButtonState extends State { } } + Future _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( + 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: [ + 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 { @override Widget build(BuildContext context) { - return _isLoading - ? Center() - : Row( - children: [ - _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(builder: (_, downloader, __) { + EpisodeTask _task = Provider.of(context, listen: false) + .episodeToTask(widget.episode); + return Row( + children: [ + _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( - Theme.of(context).accentColor), - value: task.progress / 100, + Widget _downloadButton(EpisodeTask task, BuildContext context) { + switch (task.status.value) { + case 0: + return Selector( + 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( + 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(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(Colors.red), + value: task.progress / 100, + ), ), ), ), - ), - ); - } else if (task.status == DownloadTaskStatus.complete) { - if(!widget.episodeBrief.mediaId.contains('file://')) - {_saveMediaId(task).then((episode) => - Provider.of(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(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}); -} diff --git a/lib/home/appbar/about.dart b/lib/home/appbar/about.dart index 05500ba..de34a20 100644 --- a/lib/home/appbar/about.dart +++ b/lib/home/appbar/about.dart @@ -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:?subject=Tsacdop Feedback'), + LineIcons.medium, + 'https://medium.com/@stonegate'), ], ), ), diff --git a/lib/home/appbar/addpodcast.dart b/lib/home/appbar/addpodcast.dart index 515b032..ed1076c 100644 --- a/lib/home/appbar/addpodcast.dart +++ b/lib/home/appbar/addpodcast.dart @@ -91,7 +91,6 @@ class _MyHomePageState extends State { class _MyHomePageDelegate extends SearchDelegate { static Future 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 { importOmpl.importState = ImportState.parse; await dbHelper.savePodcastRss(_p, _uuid); - + groupList.updatePodcast(podcastLocal); importOmpl.importState = ImportState.complete; } else { importOmpl.importState = ImportState.error; diff --git a/lib/home/appbar/popupmenu.dart b/lib/home/appbar/popupmenu.dart index fb4db7e..6af4e86 100644 --- a/lib/home/appbar/popupmenu.dart +++ b/lib/home/appbar/popupmenu.dart @@ -173,7 +173,7 @@ class _PopupMenuState extends State { importOmpl.importState = ImportState.parse; await dbHelper.savePodcastRss(_p, _uuid); - + groupList.updatePodcast(podcastLocal); importOmpl.importState = ImportState.complete; } else { importOmpl.importState = ImportState.error; diff --git a/lib/home/audioplayer.dart b/lib/home/audioplayer.dart index 64a959f..dbdad0e 100644 --- a/lib/home/audioplayer.dart +++ b/lib/home/audioplayer.dart @@ -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 { List minsToSelect = [1, 5, 10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99]; int _minSelected; - + final GlobalKey _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(context, listen: false); return Selector>( @@ -571,9 +570,14 @@ class _PlayerWidgetState extends State { 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 { Widget _playlist(BuildContext context) { var audio = Provider.of(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>( - 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: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 20.0), + height: 60.0, + // color: context.primaryColorDark, + alignment: Alignment.centerLeft, + child: Row( + children: [ + 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: [ - 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: [ - 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: [ - 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>( + 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: [ + Row( + children: [ + 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: [ + 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 { tabs: [ 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 { 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 { 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 { } } +class LastPosition extends StatefulWidget { + LastPosition({Key key}) : super(key: key); + + @override + _LastPositionState createState() => _LastPositionState(); +} + +class _LastPositionState extends State { + static String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } + + Future getPosition(EpisodeBrief episode) async { + var dbHelper = DBHelper(); + return await dbHelper.getPosition(episode); + } + + @override + Widget build(BuildContext context) { + var audio = Provider.of(context, listen: false); + return Selector( + selector: (_, audio) => audio.episode, + builder: (context, episode, child) { + return FutureBuilder( + 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; diff --git a/lib/home/download_list.dart b/lib/home/download_list.dart new file mode 100644 index 0000000..34b8390 --- /dev/null +++ b/lib/home/download_list.dart @@ -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(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: [ + 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 { + @override + Widget build(BuildContext context) { + return SliverPadding( + padding: EdgeInsets.all(5.0), + sliver: Consumer(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: [ + 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, + ), + ); + }), + ); + } +} diff --git a/lib/home/home_groups.dart b/lib/home/home_groups.dart index 67a5e71..4bbe733 100644 --- a/lib/home/home_groups.dart +++ b/lib/home/home_groups.dart @@ -347,7 +347,7 @@ class _PodcastPreviewState extends State { } return (snapshot.hasData) ? ShowEpisode( - podcast: snapshot.data, + episodes: snapshot.data, podcastLocal: widget.podcastLocal, ) : Center(child: CircularProgressIndicator()); @@ -400,9 +400,9 @@ class _PodcastPreviewState extends State { } class ShowEpisode extends StatelessWidget { - final List podcast; + final List 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: [ 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, ), ), ), diff --git a/lib/home/nested_home.dart b/lib/home/nested_home.dart index 53a87c3..b3f153d 100644 --- a/lib/home/nested_home.dart +++ b/lib/home/nested_home.dart @@ -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 with SingleTickerProviderStateMixin { Import(), Expanded( child: NestedScrollView( + innerScrollPositionKeyBuilder: () { + return Key('tab' + _controller.index.toString()); + }, + pinnedHeaderSliverHeightBuilder: () => 50, headerSliverBuilder: (BuildContext context, bool innerBoxScrolled) { return [ @@ -100,9 +107,12 @@ class _HomeState extends State with SingleTickerProviderStateMixin { body: TabBarView( controller: _controller, children: [ - _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> _getRssItem(int top) async { var dbHelper = DBHelper(); List 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>( future: _getRssItem(_top), builder: (context, snapshot) { @@ -367,7 +375,6 @@ class _RecentUpdateState extends State<_RecentUpdate> { slivers: [ 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> _getLikedRssItem(_top) async { var dbHelper = DBHelper(); List 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>( future: _getLikedRssItem(_top), builder: (context, snapshot) { @@ -436,7 +447,6 @@ class _MyFavoriteState extends State<_MyFavorite> { }, child: CustomScrollView( key: PageStorageKey('favorite'), - physics: const AlwaysScrollableScrollPhysics(), slivers: [ 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> _getDownloadedRssItem() async { - var dbHelper = DBHelper(); - List 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('downloas_list'), + slivers: [ + DownloadList(), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.all(15.0), + child: Text('Downloaded'), + ); + }, + childCount: 1, + ), + ), + Consumer( + 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>( - future: _getDownloadedRssItem(), - builder: (context, snapshot) { - if (snapshot.hasError) print(snapshot.error); - return (snapshot.hasData) - ? CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - primary: false, - slivers: [ - EpisodeGrid( - episodes: snapshot.data, - showDownload: true, - ) - ], - ) - : Center(child: CircularProgressIndicator()); - }, - ); - } + bool get wantKeepAlive => true; } diff --git a/lib/home/playlist.dart b/lib/home/playlist.dart index 25281ea..fef7ea4 100644 --- a/lib/home/playlist.dart +++ b/lib/home/playlist.dart @@ -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 { - final GlobalKey _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 episodes) { int sum = 0; if (episodes.length == 0) { @@ -87,13 +75,16 @@ class _PlaylistPageState extends State { ), body: SafeArea( child: - Selector, bool>>( + Selector>( selector: (_, audio) => - Tuple2(audio.queue.playlist, audio.playerRunning), + Tuple3(audio.queue, audio.playerRunning, audio.queueUpdate), builder: (_, data, __) { + print('update'); + final List episodes = data.item1.playlist; return Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, + mainAxisAlignment: MainAxisAlignment.start, children: [ Container( height: _topHeight, @@ -115,7 +106,7 @@ class _PlaylistPageState extends State { ), children: [ 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 { ), ), TextSpan( - text: data.item1.length < 2 + text: episodes.length < 2 ? ' episode ' : ' episodes ', style: TextStyle( @@ -133,7 +124,7 @@ class _PlaylistPageState extends State { )), 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 { 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: [ - 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: [ - 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: [ - (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((episode) => DismissibleContainer( + episode: episode, + key: ValueKey(episode.enclosureUrl), + )) + .toList()), ), ], ); @@ -354,3 +198,152 @@ class _PlaylistPageState extends State { ); } } + +class DismissibleContainer extends StatefulWidget { + final EpisodeBrief episode; + DismissibleContainer({this.episode, Key key}) : super(key: key); + + @override + _DismissibleContainerState createState() => _DismissibleContainerState(); +} + +class _DismissibleContainerState extends State { + 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(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: [ + 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: [ + 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.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, + // ), + ], + ), + ), + ); + } +} diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 4840355..f923886 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -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 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> getPodcastCounts(String id) async { + var dbClient = await database; + List list = await dbClient.rawQuery( + 'SELECT update_count, episode_count FROM PodcastLocal WHERE id = ?', + [id]); + return [list.first['update_count'], list.first['episode_count']]; + } + Future checkPodcast(String url) async { var dbClient = await database; List 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 getPosition(EpisodeBrief episodeBrief) async { + Future getPosition(EpisodeBrief episodeBrief) async { var dbClient = await database; List 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> getRssItem(String id, int i) async { @@ -450,7 +459,7 @@ class DBHelper { List 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 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 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 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 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 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 saveMediaId(String url, String path) async { + Future 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 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 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 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; } } diff --git a/lib/main.dart b/lib/main.dart index 767e108..d803a59 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -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(), diff --git a/lib/podcasts/podcastdetail.dart b/lib/podcasts/podcastdetail.dart index 1ed158b..70ca6f3 100644 --- a/lib/podcasts/podcastdetail.dart +++ b/lib/podcasts/podcastdetail.dart @@ -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 { 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(context, listen: false).updatePodcast(podcastLocal); + } if (mounted) setState(() {}); } @@ -400,8 +404,6 @@ class _PodcastDetailState extends State { episodes: snapshot.data, showFavorite: true, showNumber: true, - updateCount: - widget.podcastLocal.upateCount, episodeCount: widget.podcastLocal.episodeCount, ), diff --git a/lib/podcasts/podcastgroup.dart b/lib/podcasts/podcastgroup.dart index c3ba3b3..2fe1375 100644 --- a/lib/podcasts/podcastgroup.dart +++ b/lib/podcasts/podcastgroup.dart @@ -40,7 +40,7 @@ class _PodcastGroupListState extends State { 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((PodcastLocal podcastLocal) { @@ -319,7 +319,7 @@ class _PodcastCardState extends State { bottom: 20), title: Text('Remove confirm'), content: Text( - 'Are you sure you want to unsubscribe?'), + 'Are you sure you want to unsubscribe?'), actions: [ FlatButton( onPressed: () => diff --git a/lib/podcasts/podcastmanage.dart b/lib/podcasts/podcastmanage.dart index bae2067..8ea9026 100644 --- a/lib/podcasts/podcastmanage.dart +++ b/lib/podcasts/podcastmanage.dart @@ -72,7 +72,7 @@ class _PodcastManageState extends State Widget _saveButton(BuildContext context) { return Consumer( 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 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 OrderMenu(), ], ), - body: Consumer(builder: (_, groupList, __) { - bool _isLoading = groupList.isLoading; - List _groups = groupList.groups; - return _isLoading - ? Center() - : Stack( - children: [ - 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(context, listen: false).clearOrderChanged(); + return true; + }, + child: Consumer(builder: (_, groupList, __) { + bool _isLoading = groupList.isLoading; + List _groups = groupList.groups; + return _isLoading + ? Center() + : Stack( + children: [ + 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: [ - 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: [ - 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: [ + 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: [ + 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: [ - 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: [ + 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: [ - 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: [ + 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(), + ], + ); + }), + ), ), ); } diff --git a/lib/settings/settting.dart b/lib/settings/settting.dart index 2c45ec2..aa6d69a 100644 --- a/lib/settings/settting.dart +++ b/lib/settings/settting.dart @@ -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(), diff --git a/lib/settings/storage.dart b/lib/settings/storage.dart index fbc4ca1..6d49118 100644 --- a/lib/settings/storage.dart +++ b/lib/settings/storage.dart @@ -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(context, listen: false); return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Theme.of(context).accentColorBrightness, @@ -24,6 +27,54 @@ class StorageSetting extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + 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: [ + 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( + 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, diff --git a/lib/settings/syncing.dart b/lib/settings/syncing.dart index 36441ac..bcf1dbd 100644 --- a/lib/settings/syncing.dart +++ b/lib/settings/syncing.dart @@ -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, diff --git a/lib/settings/theme.dart b/lib/settings/theme.dart index 23decd1..202634a 100644 --- a/lib/settings/theme.dart +++ b/lib/settings/theme.dart @@ -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( 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( 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, ), ), ))), diff --git a/lib/util/context_extension.dart b/lib/util/context_extension.dart new file mode 100644 index 0000000..106489e --- /dev/null +++ b/lib/util/context_extension.dart @@ -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; +} \ No newline at end of file diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index 95c5868..423def9 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -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 { - _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 _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(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(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 { ); } } + + diff --git a/pubspec.yaml b/pubspec.yaml index 16e50df..9408d38 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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