diff --git a/.circleci/config.yml b/.circleci/config.yml index 965bb38..6d2077b 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -13,9 +13,7 @@ jobs: - run: name: Run Flutter doctor command: flutter doctor - - run: - name: flutter pub get - command: flutter pub get + -run: name: flutter run command: flutter run diff --git a/README.md b/README.md index 306d295..ab035e0 100644 --- a/README.md +++ b/README.md @@ -1,19 +1,21 @@ # Tsacdop [![CircleCI](https://circleci.com/gh/stonega/tsacdop.svg?style=svg)](https://circleci.com/gh/stonega/workflows/tsacdop/) ## About -![logo](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/ic_launcher.png) - -![tsacdop](https://raw.githubusercontent.com/stonega/tsacdop/master/android/app/src/main/res/mipmap-hdpi/text.png) +

+ + +

Enjoy podcasts with Tsacdop. -Tsacdop is a podcasts player developed with flutter, only for Android right now. +Tsacdop is a podcast player developed with flutter, only for Android right now. The development is still on early stage. -Credit to flutter team and involved plugin developers, especially [webfeed](https://github.com/witochandra/webfeed) and [audiofileplayer](https://github.com/google/flutter.plugins/tree/master/packages/audiofileplayer/). +Credit to flutter team and involved plugin developers, especially [webfeed](https://github.com/witochandra/webfeed) and [Just_Audio](https://pub.dev/packages/just_audio). The podcasts search engine is powered by [ListenNotes](https://listennotes.com). - +## Preview +![homepage](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_homepage.png) ![podcast](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_podcast.png) ![episode](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_episode.png) ![data](https://raw.githubusercontent.com/stonega/tsacdop/master/preview/Screenshot_data.png) ## License Tsacdop is licensed under the [MIT](https://github.com/stonega/tsacdop/blob/master/LICENSE) license. diff --git a/android/app/build.gradle b/android/app/build.gradle index 810589b..5a8e443 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -67,6 +67,7 @@ android { buildTypes { release { signingConfig signingConfigs.release + shrinkResources false } } diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index e2e874c..54d65c5 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -6,15 +6,16 @@ FlutterApplication and put your custom class here. --> + - + - + diff --git a/android/app/src/main/res/drawable-hdpi/baseline_close_white_24.png b/android/app/src/main/res/drawable-hdpi/baseline_close_white_24.png new file mode 100644 index 0000000..434cc0b Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/baseline_close_white_24.png differ diff --git a/android/app/src/main/res/drawable-hdpi/baseline_skip_next_white_24.png b/android/app/src/main/res/drawable-hdpi/baseline_skip_next_white_24.png new file mode 100644 index 0000000..af8c8c9 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/baseline_skip_next_white_24.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_action_pause.png b/android/app/src/main/res/drawable-hdpi/ic_action_pause.png new file mode 100644 index 0000000..55f33b2 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_action_pause.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_action_play_arrow.png b/android/app/src/main/res/drawable-hdpi/ic_action_play_arrow.png new file mode 100644 index 0000000..87d5743 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_action_play_arrow.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_action_skip_next.png b/android/app/src/main/res/drawable-hdpi/ic_action_skip_next.png new file mode 100644 index 0000000..28a76a3 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_action_skip_next.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_action_skip_previous.png b/android/app/src/main/res/drawable-hdpi/ic_action_skip_previous.png new file mode 100644 index 0000000..d60ff08 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_action_skip_previous.png differ diff --git a/android/app/src/main/res/drawable-hdpi/ic_action_stop.png b/android/app/src/main/res/drawable-hdpi/ic_action_stop.png new file mode 100644 index 0000000..5435114 Binary files /dev/null and b/android/app/src/main/res/drawable-hdpi/ic_action_stop.png differ diff --git a/android/app/src/main/res/drawable-mdpi/baseline_close_white_24.png b/android/app/src/main/res/drawable-mdpi/baseline_close_white_24.png new file mode 100644 index 0000000..296d24d Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/baseline_close_white_24.png differ diff --git a/android/app/src/main/res/drawable-mdpi/baseline_skip_next_white_24.png b/android/app/src/main/res/drawable-mdpi/baseline_skip_next_white_24.png new file mode 100644 index 0000000..6d8b565 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/baseline_skip_next_white_24.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_action_pause.png b/android/app/src/main/res/drawable-mdpi/ic_action_pause.png new file mode 100644 index 0000000..e8ff072 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_action_pause.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_action_play_arrow.png b/android/app/src/main/res/drawable-mdpi/ic_action_play_arrow.png new file mode 100644 index 0000000..71fff1d Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_action_play_arrow.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_action_skip_next.png b/android/app/src/main/res/drawable-mdpi/ic_action_skip_next.png new file mode 100644 index 0000000..632e4ab Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_action_skip_next.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_action_skip_previous.png b/android/app/src/main/res/drawable-mdpi/ic_action_skip_previous.png new file mode 100644 index 0000000..cf9b5f7 Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_action_skip_previous.png differ diff --git a/android/app/src/main/res/drawable-mdpi/ic_action_stop.png b/android/app/src/main/res/drawable-mdpi/ic_action_stop.png new file mode 100644 index 0000000..95e837d Binary files /dev/null and b/android/app/src/main/res/drawable-mdpi/ic_action_stop.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/baseline_close_white_24.png b/android/app/src/main/res/drawable-xhdpi/baseline_close_white_24.png new file mode 100644 index 0000000..dccd2c2 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/baseline_close_white_24.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/baseline_skip_next_white_24.png b/android/app/src/main/res/drawable-xhdpi/baseline_skip_next_white_24.png new file mode 100644 index 0000000..f515023 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/baseline_skip_next_white_24.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_action_pause.png b/android/app/src/main/res/drawable-xhdpi/ic_action_pause.png new file mode 100644 index 0000000..fbdee83 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_action_pause.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_action_play_arrow.png b/android/app/src/main/res/drawable-xhdpi/ic_action_play_arrow.png new file mode 100644 index 0000000..62d2067 Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_action_play_arrow.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_action_skip_next.png b/android/app/src/main/res/drawable-xhdpi/ic_action_skip_next.png new file mode 100644 index 0000000..164718d Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_action_skip_next.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_action_skip_previous.png b/android/app/src/main/res/drawable-xhdpi/ic_action_skip_previous.png new file mode 100644 index 0000000..b33307e Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_action_skip_previous.png differ diff --git a/android/app/src/main/res/drawable-xhdpi/ic_action_stop.png b/android/app/src/main/res/drawable-xhdpi/ic_action_stop.png new file mode 100644 index 0000000..3f7f54d Binary files /dev/null and b/android/app/src/main/res/drawable-xhdpi/ic_action_stop.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/baseline_close_white_24.png b/android/app/src/main/res/drawable-xxhdpi/baseline_close_white_24.png new file mode 100644 index 0000000..574b3d9 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/baseline_close_white_24.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/baseline_skip_next_white_24.png b/android/app/src/main/res/drawable-xxhdpi/baseline_skip_next_white_24.png new file mode 100644 index 0000000..459ace5 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/baseline_skip_next_white_24.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_action_pause.png b/android/app/src/main/res/drawable-xxhdpi/ic_action_pause.png new file mode 100644 index 0000000..8ac598d Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_action_pause.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_action_play_arrow.png b/android/app/src/main/res/drawable-xxhdpi/ic_action_play_arrow.png new file mode 100644 index 0000000..47ce73a Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_action_play_arrow.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_next.png b/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_next.png new file mode 100644 index 0000000..0f3b6a1 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_next.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_previous.png b/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_previous.png new file mode 100644 index 0000000..2e09dfa Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_action_skip_previous.png differ diff --git a/android/app/src/main/res/drawable-xxhdpi/ic_action_stop.png b/android/app/src/main/res/drawable-xxhdpi/ic_action_stop.png new file mode 100644 index 0000000..17da4a3 Binary files /dev/null and b/android/app/src/main/res/drawable-xxhdpi/ic_action_stop.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/baseline_close_white_36.png b/android/app/src/main/res/drawable-xxxhdpi/baseline_close_white_36.png new file mode 100644 index 0000000..de87814 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/baseline_close_white_36.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/baseline_skip_next_white_24.png b/android/app/src/main/res/drawable-xxxhdpi/baseline_skip_next_white_24.png new file mode 100644 index 0000000..caaf796 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/baseline_skip_next_white_24.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_action_pause.png b/android/app/src/main/res/drawable-xxxhdpi/ic_action_pause.png new file mode 100644 index 0000000..4343502 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_action_pause.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_action_play_arrow.png b/android/app/src/main/res/drawable-xxxhdpi/ic_action_play_arrow.png new file mode 100644 index 0000000..e9f9281 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_action_play_arrow.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_next.png b/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_next.png new file mode 100644 index 0000000..ea1a197 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_next.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_previous.png b/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_previous.png new file mode 100644 index 0000000..3dcbcd4 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_action_skip_previous.png differ diff --git a/android/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png b/android/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png new file mode 100644 index 0000000..20ee1b7 Binary files /dev/null and b/android/app/src/main/res/drawable-xxxhdpi/ic_action_stop.png differ diff --git a/android/app/src/main/res/mipmap-hdpi/text_light.png b/android/app/src/main/res/mipmap-hdpi/text_light.png new file mode 100644 index 0000000..738e4de Binary files /dev/null and b/android/app/src/main/res/mipmap-hdpi/text_light.png differ diff --git a/android/app/src/main/res/mipmap-mdpi/text_light.png b/android/app/src/main/res/mipmap-mdpi/text_light.png new file mode 100644 index 0000000..1f9e204 Binary files /dev/null and b/android/app/src/main/res/mipmap-mdpi/text_light.png differ diff --git a/android/app/src/main/res/mipmap-xhdpi/text_light.png b/android/app/src/main/res/mipmap-xhdpi/text_light.png new file mode 100644 index 0000000..f9a0f77 Binary files /dev/null and b/android/app/src/main/res/mipmap-xhdpi/text_light.png differ diff --git a/android/app/src/main/res/mipmap-xxhdpi/text_light.png b/android/app/src/main/res/mipmap-xxhdpi/text_light.png new file mode 100644 index 0000000..64c52e1 Binary files /dev/null and b/android/app/src/main/res/mipmap-xxhdpi/text_light.png differ diff --git a/android/app/src/main/res/mipmap-xxxhdpi/text_light.png b/android/app/src/main/res/mipmap-xxxhdpi/text_light.png new file mode 100644 index 0000000..e35a80e Binary files /dev/null and b/android/app/src/main/res/mipmap-xxxhdpi/text_light.png differ diff --git a/android/app/src/main/res/values/colors.xml b/android/app/src/main/res/values/colors.xml new file mode 100644 index 0000000..81e5bcf --- /dev/null +++ b/android/app/src/main/res/values/colors.xml @@ -0,0 +1,6 @@ + + + + #121212 + + \ No newline at end of file diff --git a/android/app/src/main/res/xml/network_security_config.xml b/android/app/src/main/res/xml/network_security_config.xml index caf1945..f18e1f0 100644 --- a/android/app/src/main/res/xml/network_security_config.xml +++ b/android/app/src/main/res/xml/network_security_config.xml @@ -1,7 +1,8 @@ - - lizhi.fm - xmcdn.com - + + + + + \ No newline at end of file diff --git a/assets/listennotes_light.png b/assets/listennotes_light.png new file mode 100644 index 0000000..8fbee9d Binary files /dev/null and b/assets/listennotes_light.png differ diff --git a/assets/text_light.png b/assets/text_light.png new file mode 100644 index 0000000..e35a80e Binary files /dev/null and b/assets/text_light.png differ diff --git a/lib/class/audiostate.dart b/lib/class/audiostate.dart index da0cc46..9893dab 100644 --- a/lib/class/audiostate.dart +++ b/lib/class/audiostate.dart @@ -1,17 +1,46 @@ -import 'dart:typed_data'; import 'dart:async'; -import 'dart:io'; import 'package:flutter/foundation.dart'; +import 'package:audio_service/audio_service.dart'; +import 'package:just_audio/just_audio.dart'; import 'package:tsacdop/class/episodebrief.dart'; -import 'package:audiofileplayer/audiofileplayer.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; -import 'package:logging/logging.dart'; -import 'package:audiofileplayer/audio_system.dart'; import 'package:tsacdop/local_storage/key_value_storage.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; -//enum AudioState { load, play, pause, complete, error, stop } +MediaControl playControl = MediaControl( + androidIcon: 'drawable/ic_stat_play_circle_filled', + label: 'Play', + action: MediaAction.play, +); +MediaControl pauseControl = MediaControl( + androidIcon: 'drawable/ic_stat_pause_circle_filled', + label: 'Pause', + action: MediaAction.pause, +); +MediaControl skipToNextControl = MediaControl( + androidIcon: 'drawable/baseline_skip_next_white_24', + label: 'Next', + action: MediaAction.skipToNext, +); +MediaControl skipToPreviousControl = MediaControl( + androidIcon: 'drawable/ic_action_skip_previous', + label: 'Previous', + action: MediaAction.skipToPrevious, +); +MediaControl stopControl = MediaControl( + androidIcon: 'drawable/baseline_close_white_24', + label: 'Stop', + action: MediaAction.stop, +); +MediaControl forward30 = MediaControl( + androidIcon: 'drawable/ic_stat_forward_30', + label: 'forward30', + action: MediaAction.fastForward, +); + +void _audioPlayerTaskEntrypoint() async { + AudioServiceBackground.run(() => AudioPlayerTask()); +} class PlayHistory { DBHelper dbHelper = DBHelper(); @@ -19,7 +48,9 @@ class PlayHistory { String url; double seconds; double seekValue; - PlayHistory(this.title, this.url, this.seconds, this.seekValue); + DateTime playdate; + PlayHistory(this.title, this.url, this.seconds, this.seekValue, + {this.playdate}); EpisodeBrief _episode; EpisodeBrief get episode => _episode; @@ -31,32 +62,36 @@ class PlayHistory { class Playlist { String name; DBHelper dbHelper = DBHelper(); - List urls; + // list of urls + //List _urls; + //list of episodes List _playlist; + //list of miediaitem + List get playlist => _playlist; KeyValueStorage storage = KeyValueStorage('playlist'); - Playlist(this.name, {List urls}) : urls = urls ?? []; getPlaylist() async { - List _urls = await storage.getStringList(); - if (_urls.length == 0) { + List urls = await storage.getStringList(); + print(urls); + if (urls.length == 0) { _playlist = []; } else { _playlist = []; - await Future.forEach(_urls, (url) async { + await Future.forEach(urls, (url) async { EpisodeBrief episode = await dbHelper.getRssItemWithUrl(url); print(episode.title); _playlist.add(episode); }); } - print(_playlist.length); + print('Playlist: ' + _playlist.length.toString()); } savePlaylist() async { - urls = []; + List urls = []; urls.addAll(_playlist.map((e) => e.enclosureUrl)); print(urls); - await storage.saveStringlist(urls); + await storage.saveStringList(urls); } addToPlayList(EpisodeBrief episodeBrief) async { @@ -64,6 +99,11 @@ class Playlist { await savePlaylist(); } + addToPlayListAt(EpisodeBrief episodeBrief, int index) async { + _playlist.insert(index, episodeBrief); + await savePlaylist(); + } + delFromPlaylist(EpisodeBrief episodeBrief) async { _playlist .removeWhere((item) => item.enclosureUrl == episodeBrief.enclosureUrl); @@ -71,39 +111,32 @@ class Playlist { } } -class AudioPlayer extends ChangeNotifier { - static const String replay10ButtonId = 'replay10ButtonId'; - static const String newReleasesButtonId = 'newReleasesButtonId'; - static const String likeButtonId = 'likeButtonId'; - static const String pausenowButtonId = 'pausenowButtonId'; - static const String forwardButtonId = 'forwardButtonId'; - +class AudioPlayerNotifier extends ChangeNotifier { DBHelper dbHelper = DBHelper(); KeyValueStorage storage = KeyValueStorage('audioposition'); EpisodeBrief _episode; - Playlist _queue = Playlist('now'); + Playlist _queue = Playlist(); + BasicPlaybackState _audioState = BasicPlaybackState.none; bool _playerRunning = false; - Audio _backgroundAudio; - bool _backgroundAudioPlaying = false; - double _backgroundAudioDurationSeconds = 0; - double _backgroundAudioPositionSeconds = 0; - bool _remoteAudioLoading = false; + bool _noSlide = true; + int _backgroundAudioDuration = 0; + int _backgroundAudioPosition = 0; String _remoteErrorMessage; + double _seekSliderValue = 0.0; - int _lastPostion; - bool _skip = false; + int _lastPostion = 0; bool _stopOnComplete = false; Timer _stopTimer; //Show stopwatch after user setting timer. - bool _showStopWatch = false; - - - final Logger _logger = Logger('audiofileplayer'); + bool _showStopWatch = false; + bool _autoPlay = true; + DateTime _current; + int _currentPosition; - bool get backgroundAudioPlaying => _backgroundAudioPlaying; - bool get remoteAudioLoading => _remoteAudioLoading; - double get backgroundAudioDuration => _backgroundAudioDurationSeconds; - double get backgroundAudioPosition => _backgroundAudioPositionSeconds; + BasicPlaybackState get audioState => _audioState; + + int get backgroundAudioDuration => _backgroundAudioDuration; + int get backgroundAudioPosition => _backgroundAudioPosition; double get seekSliderValue => _seekSliderValue; String get remoteErrorMessage => _remoteErrorMessage; bool get playerRunning => _playerRunning; @@ -112,385 +145,470 @@ class AudioPlayer extends ChangeNotifier { EpisodeBrief get episode => _episode; bool get stopOnComplete => _stopOnComplete; bool get showStopWatch => _showStopWatch; - + bool get autoPlay => _autoPlay; set setStopOnComplete(bool boo) { _stopOnComplete = boo; } + set autoPlaySwitch(bool boo) { + _autoPlay = boo; + notifyListeners(); + } + + @override + void addListener(VoidCallback listener) async { + super.addListener(listener); + await AudioService.connect(); + } + loadPlaylist() async { await _queue.getPlaylist(); _lastPostion = await storage.getInt(); } episodeLoad(EpisodeBrief episode) async { - if (_playerRunning && _episode != null) { + if (_playerRunning) { PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, - backgroundAudioDuration, seekSliderValue); + backgroundAudioPosition / 1000, seekSliderValue); await dbHelper.saveHistory(history); + AudioService.addQueueItemAt(episode.toMediaItem(), 0); + _queue.playlist + .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl); + _queue.playlist.insert(0, episode); + notifyListeners(); + await _queue.savePlaylist(); + } else { + await _queue.getPlaylist(); + _queue.playlist + .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl); + _queue.playlist.insert(0, episode); + _queue.savePlaylist(); + _backgroundAudioDuration = 0; + _backgroundAudioPosition = 0; + _seekSliderValue = 0; + _episode = episode; + _playerRunning = true; + notifyListeners(); + await _queue.savePlaylist(); + _startAudioService(0); } - AudioSystem.instance.addMediaEventListener(_mediaEventListener); - _backgroundAudioPlaying = false; - _episode = episode; - await _queue.getPlaylist(); - _queue.playlist - .removeWhere((item) => item.enclosureUrl == _episode.enclosureUrl); - _queue.playlist.insert(0, _episode); - await _queue.savePlaylist(); - await _play(_episode); + } + + _startAudioService(int position) async { + if (!AudioService.connected) { + await AudioService.connect(); + } + await AudioService.start( + backgroundTaskEntrypoint: _audioPlayerTaskEntrypoint, + androidNotificationChannelName: 'Tsacdop', + notificationColor: 0xFF2196f3, + androidNotificationIcon: 'mipmap/ic_launcher', + enableQueue: true, + androidStopOnRemoveTask: true, + ); _playerRunning = true; - notifyListeners(); + if (autoPlay) { + await Future.forEach(_queue.playlist, (episode) async { + await AudioService.addQueueItem(episode.toMediaItem()); + }); + } else { + await AudioService.addQueueItem(_queue.playlist.first.toMediaItem()); + } + await AudioService.play(); + AudioService.currentMediaItemStream.listen((item) async { + print(position); + print(_backgroundAudioDuration); + if (item != null) { + _episode = await dbHelper.getRssItemWithMediaId(item.id); + _backgroundAudioDuration = item?.duration ?? 0; + if (position > 0 && _backgroundAudioDuration > 0) { + AudioService.seekTo(position); + position = 0; + } + // _playerRunning = true; + } + notifyListeners(); + }); + AudioService.playbackStateStream.listen((event) async { + _current = DateTime.now(); + _audioState = event?.basicState; + if (_audioState == BasicPlaybackState.skippingToNext && + _episode != null) { + _queue.delFromPlaylist(_episode); + } + if (_audioState == BasicPlaybackState.paused || + _audioState == BasicPlaybackState.skippingToNext && + _episode != null) { + PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, + backgroundAudioPosition / 1000, seekSliderValue); + await dbHelper.saveHistory(history); + } + if (_audioState == BasicPlaybackState.stopped) { + _playerRunning = false; + } + _currentPosition = event?.currentPosition ?? 0; + notifyListeners(); + }); + Timer.periodic(Duration(milliseconds: 500), (timer) { + if (_noSlide) { + _audioState == BasicPlaybackState.playing + ? (_backgroundAudioPosition < _backgroundAudioDuration) + ? _backgroundAudioPosition = _currentPosition + + DateTime.now().difference(_current).inMilliseconds + : _backgroundAudioPosition = _backgroundAudioDuration + : _backgroundAudioPosition = _currentPosition; + + if (_backgroundAudioDuration != null && + _backgroundAudioDuration != 0 && + _backgroundAudioPosition != null) { + _seekSliderValue = + _backgroundAudioPosition / _backgroundAudioDuration ?? 0; + } + if (_backgroundAudioPosition > 0) { + _lastPostion = _backgroundAudioPosition; + storage.saveInt(_lastPostion); + } + notifyListeners(); + } + if (_audioState == BasicPlaybackState.stopped) { + timer.cancel(); + } + }); } playlistLoad() async { - _backgroundAudioPlaying = false; await _queue.getPlaylist(); + _backgroundAudioDuration = 0; + _backgroundAudioPosition = 0; + _seekSliderValue = 0; _episode = _queue.playlist.first; - _skip = true; - await _play(_episode); _playerRunning = true; notifyListeners(); + _startAudioService(_lastPostion ?? 0); } playNext() async { - storage.saveInt(0); - _lastPostion = 0; - PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, - backgroundAudioDuration, seekSliderValue); - await dbHelper.saveHistory(history); - await _queue.delFromPlaylist(_episode); - if (_queue.playlist.length > 0 && !_stopOnComplete) { - playlistLoad(); - } else { - _stopOnComplete = false; - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - _playerRunning = false; - _disposeAudio(); - notifyListeners(); - } + AudioService.skipToNext(); } addToPlaylist(EpisodeBrief episode) async { - _queue.addToPlayList(episode); - await _queue.getPlaylist(); + if (_playerRunning) { + await AudioService.addQueueItem(episode.toMediaItem()); + } + print('add to playlist when not rnnning'); + await _queue.addToPlayList(episode); notifyListeners(); } + addToPlaylistAt(EpisodeBrief episode, int index) async { + if (_playerRunning) { + await AudioService.addQueueItemAt(episode.toMediaItem(), index); + } + print('add to playlist when not rnnning'); + await _queue.addToPlayListAt(episode, index); + notifyListeners(); + } + + updateMediaItem(EpisodeBrief episode) async { + int index = _queue.playlist + .indexWhere((item) => item.enclosureUrl == episode.enclosureUrl); + if (index > 0) { + await delFromPlaylist(episode); + await addToPlaylistAt(episode, index); + } + } + delFromPlaylist(EpisodeBrief episode) async { - _queue.delFromPlaylist(episode); - await _queue.getPlaylist(); + if (_playerRunning) { + await AudioService.removeQueueItem(episode.toMediaItem()); + } + await _queue.delFromPlaylist(episode); notifyListeners(); } pauseAduio() async { - _pauseBackgroundAudio(); - notifyListeners(); - PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, - backgroundAudioPosition, seekSliderValue); - await dbHelper.saveHistory(history); + AudioService.pause(); } - resumeAudio() { - _resumeBackgroundAudio(); - notifyListeners(); + resumeAudio() async { + AudioService.play(); } - forwardAudio(double s) { - _forwardBackgroundAudio(s); - notifyListeners(); + forwardAudio(int s) { + int pos = _backgroundAudioPosition + s * 1000; + AudioService.seekTo(pos); } - sliderSeek(double val) { + sliderSeek(double val) async { + print(val.toString()); + _noSlide = false; _seekSliderValue = val; notifyListeners(); - final double positionSeconds = val * _backgroundAudioDurationSeconds; - _backgroundAudio.seek(positionSeconds); - AudioSystem.instance.setPlaybackState(true, positionSeconds); + _currentPosition = (val * _backgroundAudioDuration).toInt(); + await AudioService.seekTo(_currentPosition); + _noSlide = true; } - //Set sleep time + + //Set sleep time sleepTimer(int mins) { _showStopWatch = true; notifyListeners(); - _stopTimer = Timer(Duration(minutes: mins),(){ + _stopTimer = Timer(Duration(minutes: mins), () { _stopOnComplete = false; - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - _playerRunning = false; _showStopWatch = false; - _disposeAudio(); + AudioService.stop(); notifyListeners(); }); } + //Cancel sleep timer - cancelTimer(){ + cancelTimer() { _stopTimer.cancel(); _showStopWatch = false; notifyListeners(); } - _disposeAudio() { - pauseAduio(); - AudioSystem.instance?.stopBackgroundDisplay(); - AudioSystem.instance?.removeMediaEventListener(_mediaEventListener); - _backgroundAudio?.dispose(); + @override + void dispose() async { + await AudioService.stop(); + await AudioService.disconnect(); + super.dispose(); + } +} + +class AudioPlayerTask extends BackgroundAudioTask { + List _queue = []; + AudioPlayer _audioPlayer = AudioPlayer(); + Completer _completer = Completer(); + BasicPlaybackState _skipState; + bool _playing; + + bool get hasNext => _queue.length > 0; + + MediaItem get mediaItem => _queue.first; + + BasicPlaybackState _stateToBasicState(AudioPlaybackState state) { + switch (state) { + case AudioPlaybackState.none: + return BasicPlaybackState.none; + case AudioPlaybackState.stopped: + return BasicPlaybackState.stopped; + case AudioPlaybackState.paused: + return BasicPlaybackState.paused; + case AudioPlaybackState.playing: + return BasicPlaybackState.playing; + case AudioPlaybackState.connecting: + return _skipState ?? BasicPlaybackState.connecting; + case AudioPlaybackState.completed: + return BasicPlaybackState.stopped; + default: + throw Exception("Illegal state"); + } } @override - dispose() { - _disposeAudio(); - super.dispose(); - } - - _play(EpisodeBrief episodeBrief) async { - AudioSystem.instance.addMediaEventListener(_mediaEventListener); - String url = _queue.playlist.first.enclosureUrl; - _getFile(url).then((result) { - result == 'NotDownload' - ? _initbackgroundAudioPlayer(url) - : _initbackgroundAudioPlayerLocal(result); + Future onStart() async { + print('start background task'); + var playerStateSubscription = _audioPlayer.playbackStateStream + .where((state) => state == AudioPlaybackState.completed) + .listen((state) { + _handlePlaybackCompleted(); }); - } - - Future _getFile(String url) async { - final task = await FlutterDownloader.loadTasksWithRawQuery( - query: "SELECT * FROM task WHERE url = '$url' AND status = 3"); - if (task.length != 0) { - String _filePath = task.first.savedDir + '/' + task.first.filename; - return _filePath; - } - return 'NotDownload'; - } - - ByteData _getAudio(String path) { - File audioFile = File(path); - Uint8List audio = audioFile.readAsBytesSync(); - return ByteData.view(audio.buffer); - } - - onDuration(double durationSeconds) { - _backgroundAudioDurationSeconds = durationSeconds; - _remoteAudioLoading = false; - _backgroundAudioPlaying = true; - if (_skip) { - _forwardBackgroundAudio(_lastPostion.toDouble()); - _backgroundAudioPositionSeconds = _lastPostion.toDouble(); - } - _skip = false; - _setNotification(true); - notifyListeners(); - } - - onPosition(double positionSeconds) { - if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) { - _seekSliderValue = - _backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds; - _backgroundAudioPositionSeconds = positionSeconds; - notifyListeners(); - } else { - _seekSliderValue = 1; - _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; - notifyListeners(); - } - _lastPostion = positionSeconds.toInt(); - storage.saveInt(_lastPostion); - } - - onError(String message) { - _remoteErrorMessage = message; - _backgroundAudio.dispose(); - _backgroundAudio = null; - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - } - - void _initbackgroundAudioPlayerLocal(String path) { - _remoteErrorMessage = null; - _remoteAudioLoading = true; - ByteData audio = _getAudio(path); - _backgroundAudio?.pause(); - _backgroundAudioPositionSeconds = 0; - _setNotification(false); - _backgroundAudio = Audio.loadFromByteData(audio, - onDuration: (double durationSeconds) => onDuration(durationSeconds), - onPosition: (double positionSeconds) => onPosition(positionSeconds), - onError: (String message) => onError(message), - onComplete: () => playNext(), - looping: false, - playInBackground: true) - ..play(); - } - - void _initbackgroundAudioPlayer(String url) { - _remoteErrorMessage = null; - _remoteAudioLoading = true; - notifyListeners(); - _backgroundAudio?.pause(); - _backgroundAudioPositionSeconds = 0; - _setNotification(false); - _backgroundAudio = Audio.loadFromRemoteUrl(url, - onDuration: (double durationSeconds) => onDuration(durationSeconds), - onPosition: (double positionSeconds) => onPosition(positionSeconds), - onError: (String message) => onError(message), - onComplete: () => playNext(), - looping: false, - playInBackground: true) - ..resume(); - } - - void _mediaEventListener(MediaEvent mediaEvent) { - _logger.info('App received media event of type: ${mediaEvent.type}'); - final MediaActionType type = mediaEvent.type; - if (type == MediaActionType.play) { - _resumeBackgroundAudio(); - } else if (type == MediaActionType.pause) { - _pauseBackgroundAudio(); - } else if (type == MediaActionType.playPause) { - _backgroundAudioPlaying - ? _pauseBackgroundAudio() - : _resumeBackgroundAudio(); - } else if (type == MediaActionType.stop) { - _stopBackgroundAudio(); - } else if (type == MediaActionType.seekTo) { - _backgroundAudio.seek(mediaEvent.seekToPositionSeconds); - AudioSystem.instance - .setPlaybackState(true, mediaEvent.seekToPositionSeconds); - } else if (type == MediaActionType.skipForward) { - final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; - _forwardBackgroundAudio(skipIntervalSeconds); - _logger.info( - 'Skip-forward event had skipIntervalSeconds $skipIntervalSeconds.'); - } else if (type == MediaActionType.skipBackward) { - final double skipIntervalSeconds = mediaEvent.skipIntervalSeconds; - _forwardBackgroundAudio(skipIntervalSeconds); - _logger.info( - 'Skip-backward event had skipIntervalSeconds $skipIntervalSeconds.'); - } else if (type == MediaActionType.custom) { - if (mediaEvent.customEventId == replay10ButtonId) { - _forwardBackgroundAudio(-10.0); - } else if (mediaEvent.customEventId == likeButtonId) { - _resumeBackgroundAudio(); - } else if (mediaEvent.customEventId == forwardButtonId) { - _forwardBackgroundAudio(30.0); - } else if (mediaEvent.customEventId == pausenowButtonId) { - _pauseBackgroundAudio(); + var eventSubscription = _audioPlayer.playbackEventStream.listen((event) { + BasicPlaybackState state; + if (event.buffering) { + state = BasicPlaybackState.buffering; + } else { + state = _stateToBasicState(event.state); } + if (state != BasicPlaybackState.stopped) { + _setState( + state: state, + position: event.position.inMilliseconds, + ); + } + }); + await _completer.future; + playerStateSubscription.cancel(); + eventSubscription.cancel(); + } + + void _handlePlaybackCompleted() { + if (hasNext) { + onSkipToNext(); + } else { + onStop(); } } - Future _setNotification(bool boo) async { - final Uint8List imageBytes = - File('${_episode.imagePath}').readAsBytesSync(); - AudioSystem.instance.setMetadata(AudioMetadata( - title: episode.title, - artist: episode.feedTitle, - album: episode.feedTitle, - genre: "Podcast", - durationSeconds: _backgroundAudioDurationSeconds, - artBytes: imageBytes)); - AudioSystem.instance.setPlaybackState(boo, _backgroundAudioPositionSeconds); - AudioSystem.instance.setAndroidNotificationButtons([ - AndroidMediaButtonType.pause, - _forwardButton, - AndroidMediaButtonType.stop, - ], androidCompactIndices: [ - 0, - 1 - ]); - - AudioSystem.instance.setSupportedMediaActions({ - MediaActionType.playPause, - MediaActionType.pause, - MediaActionType.next, - MediaActionType.previous, - MediaActionType.skipForward, - MediaActionType.skipBackward, - MediaActionType.seekTo, - MediaActionType.custom, - }, skipIntervalSeconds: 30); + void playPause() { + if (AudioServiceBackground.state.basicState == BasicPlaybackState.playing) + onPause(); + else + onPlay(); } - Future _resumeBackgroundAudio() async { - _backgroundAudio.resume(); - - _backgroundAudioPlaying = true; - - final Uint8List imageBytes = - File('${_episode.imagePath}').readAsBytesSync(); - AudioSystem.instance.setMetadata(AudioMetadata( - title: _episode.title, - artist: _episode.feedTitle, - album: _episode.feedTitle, - genre: "Podcast", - durationSeconds: _backgroundAudioDurationSeconds, - artBytes: imageBytes)); - - AudioSystem.instance - .setPlaybackState(true, _backgroundAudioPositionSeconds); - - AudioSystem.instance.setAndroidNotificationButtons([ - AndroidMediaButtonType.pause, - _forwardButton, - AndroidMediaButtonType.stop, - ], androidCompactIndices: [ - 0, - 1 - ]); - - AudioSystem.instance.setSupportedMediaActions({ - MediaActionType.playPause, - MediaActionType.pause, - MediaActionType.next, - MediaActionType.previous, - MediaActionType.skipForward, - MediaActionType.skipBackward, - MediaActionType.seekTo, - MediaActionType.custom, - }, skipIntervalSeconds: 30); + @override + Future onSkipToNext() async { + if (_playing == null) { + // First time, we want to start playing + _playing = true; + } else { + // Stop current item + await _audioPlayer.stop(); + _queue.removeAt(0); + } + AudioServiceBackground.setQueue(_queue); + AudioServiceBackground.setMediaItem(mediaItem); + _skipState = BasicPlaybackState.skippingToNext; + await _audioPlayer.setUrl(mediaItem.id); + print(mediaItem.id); + Duration duration = await _audioPlayer.durationFuture; + AudioServiceBackground.setMediaItem( + mediaItem.copyWith(duration: duration.inMilliseconds)); + _skipState = null; + // Resume playback if we were playing + if (_playing) { + onPlay(); + } else { + _setState(state: BasicPlaybackState.paused); + } } - void _pauseBackgroundAudio() { - _backgroundAudio?.pause(); - _backgroundAudioPlaying = false; - AudioSystem.instance - .setPlaybackState(false, _backgroundAudioPositionSeconds); - AudioSystem.instance.setAndroidNotificationButtons([ - AndroidMediaButtonType.play, - _forwardButton, - AndroidMediaButtonType.stop, - ], androidCompactIndices: [ - 0, - 1, - ]); - - AudioSystem.instance.setSupportedMediaActions({ - MediaActionType.playPause, - MediaActionType.play, - MediaActionType.next, - MediaActionType.previous, - }); + @override + void onPlay() async { + if (_skipState == null) { + if (_playing == null) { + _playing = true; + AudioServiceBackground.setQueue(_queue); + await _audioPlayer.setUrl(mediaItem.id); + Duration duration = await _audioPlayer.durationFuture; + AudioServiceBackground.setMediaItem( + mediaItem.copyWith(duration: duration.inMilliseconds)); + } + _playing = true; + _audioPlayer.play(); + } } - void _stopBackgroundAudio() { - _backgroundAudio.pause(); - _backgroundAudio.dispose(); - _backgroundAudioPlaying = false; - AudioSystem.instance.stopBackgroundDisplay(); + @override + void onPause() { + if (_skipState == null) { + if (_playing == null) {} + _playing = false; + _audioPlayer.pause(); + } } - void _forwardBackgroundAudio(double seconds) { - final double forwardposition = _backgroundAudioPositionSeconds + seconds; - _backgroundAudio.seek(forwardposition); - AudioSystem.instance - .setPlaybackState(true, _backgroundAudioPositionSeconds); + @override + void onSeekTo(int position) { + _audioPlayer.seek(Duration(milliseconds: position)); } - final _pauseButton = AndroidCustomMediaButton( - 'pausenow', pausenowButtonId, 'ic_stat_pause_circle_filled'); - final _replay10Button = AndroidCustomMediaButton( - 'replay10', replay10ButtonId, 'ic_stat_replay_10'); - final _forwardButton = AndroidCustomMediaButton( - 'forward', forwardButtonId, 'ic_stat_forward_30'); - final _playnowButton = AndroidCustomMediaButton( - 'playnow', likeButtonId, 'ic_stat_play_circle_filled'); + @override + void onClick(MediaButton button) { + playPause(); + } + + @override + void onStop() async { + await _audioPlayer.stop(); + _setState(state: BasicPlaybackState.stopped); + _completer.complete(); + } + + @override + void onAddQueueItem(MediaItem mediaItem) async { + _queue.add(mediaItem); + AudioServiceBackground.setQueue(_queue); + } + + @override + void onRemoveQueueItem(MediaItem mediaItem) async { + _queue.removeWhere((item) => item.id == mediaItem.id); + await AudioServiceBackground.setQueue(_queue); + } + + @override + void onAddQueueItemAt(MediaItem mediaItem, int index) async { + if (index == 0) { + await _audioPlayer.stop(); + _queue.removeWhere((item) => item.id == mediaItem.id); + _queue.insert(0, mediaItem); + AudioServiceBackground.setQueue(_queue); + AudioServiceBackground.setMediaItem(mediaItem); + await _audioPlayer.setUrl(mediaItem.id); + Duration duration = await _audioPlayer.durationFuture; + AudioServiceBackground.setMediaItem( + mediaItem.copyWith(duration: duration.inMilliseconds)); + onPlay(); + } else { + _queue.insert(index, mediaItem); + AudioServiceBackground.setQueue(_queue); + } + } + + @override + void onFastForward() { + _audioPlayer.seek(Duration( + milliseconds: AudioServiceBackground.state.position + 30 * 1000)); + } + + @override + void onAudioFocusLost() { + if (_skipState == null) { + if (_playing == null) {} + _playing = false; + _audioPlayer.pause(); + } + } + + @override + void onAudioBecomingNoisy() { + if (_skipState == null) { + if (_playing == null) {} + _playing = false; + _audioPlayer.pause(); + } + } + + @override + void onAudioFocusGained() { + if (_skipState == null) { + if (_playing == null) {} + _playing = true; + _audioPlayer.play(); + } + } + + @override + void onCustomAction(funtion, argument) { + switch (funtion) { + case 'addQueue': + break; + case 'updateMedia': + break; + } + } + + void _setState({@required BasicPlaybackState state, int position}) { + if (position == null) { + position = _audioPlayer.playbackEvent.position.inMilliseconds; + } + AudioServiceBackground.setState( + controls: getControls(state), + systemActions: [MediaAction.seekTo], + basicState: state, + position: position, + ); + } + + List getControls(BasicPlaybackState state) { + if (_playing) { + return [pauseControl, forward30, skipToNextControl, stopControl]; + } else { + return [playControl, forward30, skipToNextControl, stopControl]; + } + } } diff --git a/lib/class/episodebrief.dart b/lib/class/episodebrief.dart index d76bb39..8427652 100644 --- a/lib/class/episodebrief.dart +++ b/lib/class/episodebrief.dart @@ -1,4 +1,5 @@ import 'package:intl/intl.dart'; +import 'package:audio_service/audio_service.dart'; class EpisodeBrief { final String title; @@ -13,6 +14,7 @@ class EpisodeBrief { final int duration; final int explicit; final String imagePath; + final String mediaId; EpisodeBrief( this.title, this.enclosureUrl, @@ -21,21 +23,31 @@ class EpisodeBrief { this.feedTitle, this.primaryColor, this.liked, - this.downloaded, + this.downloaded, this.duration, this.explicit, - this.imagePath - ); + this.imagePath, + this.mediaId); - String dateToString(){ - DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate); + String dateToString() { + DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true); var diffrence = DateTime.now().difference(date); - if(diffrence.inHours < 24) { + if (diffrence.inHours < 24) { return '${diffrence.inHours} hours ago'; - } else if (diffrence.inDays < 7){ - return '${diffrence.inDays} days ago';} - else { - return DateFormat.yMMMd().format( DateTime.fromMillisecondsSinceEpoch(pubDate)); - } + } else if (diffrence.inDays < 7) { + return '${diffrence.inDays} days ago'; + } else { + return DateFormat.yMMMd() + .format(DateTime.fromMillisecondsSinceEpoch(pubDate)); } + } + + MediaItem toMediaItem() { + return MediaItem( + id: mediaId, + title: title, + artist: feedTitle, + album: feedTitle, + artUri: 'file://$imagePath'); + } } diff --git a/lib/class/podcast_group.dart b/lib/class/podcast_group.dart index 711cca8..7a79db4 100644 --- a/lib/class/podcast_group.dart +++ b/lib/class/podcast_group.dart @@ -55,8 +55,12 @@ class PodcastGroup { } List _podcasts; - + List _orderedPodcasts; + List get ordereddPodcasts => _orderedPodcasts; List get podcasts => _podcasts; + set setOrderedPodcasts(List list) { + _orderedPodcasts = list; + } GroupEntity toEntity() { return GroupEntity(name, id, color, podcastList); @@ -82,9 +86,20 @@ class GroupList extends ChangeNotifier { GroupList({List groups}) : _groups = groups ?? []; bool _isLoading = false; - bool get isLoading => _isLoading; + List _orderChanged = []; + List get orderChanged => _orderChanged; + void addToOrderChanged(String name) { + _orderChanged.add(name); + notifyListeners(); + } + + void drlFromOrderChanged(String name) { + _orderChanged.remove(name); + notifyListeners(); + } + @override void addListener(VoidCallback listener) { super.addListener(listener); @@ -105,14 +120,15 @@ class GroupList extends ChangeNotifier { } Future addGroup(PodcastGroup podcastGroup) async { + _isLoading = true; _groups.add(podcastGroup); _saveGroup(); + _isLoading = false; notifyListeners(); } Future delGroup(PodcastGroup podcastGroup) async { _isLoading = true; - notifyListeners(); podcastGroup.podcastList.forEach((podcast) { if (!_groups.first.podcastList.contains(podcast)) { _groups[0].podcastList.insert(0, podcast); @@ -121,11 +137,11 @@ class GroupList extends ChangeNotifier { _saveGroup(); _groups.remove(podcastGroup); await _groups[0].getPodcasts(); - _isLoading = false; + _isLoading = false; notifyListeners(); } - updateGroup(PodcastGroup podcastGroup) async{ + updateGroup(PodcastGroup podcastGroup) async { var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id); var index = _groups.indexOf(oldGroup); _groups.replaceRange(index, index + 1, [podcastGroup]); @@ -161,7 +177,11 @@ class GroupList extends ChangeNotifier { _isLoading = true; notifyListeners(); getPodcastGroup(id).forEach((group) { - group.podcastList.remove(id); + if (list.contains(group)) { + list.remove(group); + } else { + group.podcastList.remove(id); + } }); list.forEach((s) { s.podcastList.insert(0, id); @@ -190,8 +210,8 @@ class GroupList extends ChangeNotifier { notifyListeners(); } - saveOrder(PodcastGroup group, List podcasts) async { - group.podcastList = podcasts.map((e) => e.id).toList(); + saveOrder(PodcastGroup group) async { + group.podcastList = group.ordereddPodcasts.map((e) => e.id).toList(); _saveGroup(); await group.getPodcasts(); notifyListeners(); diff --git a/lib/class/settingstate.dart b/lib/class/settingstate.dart index fbe8631..61dd504 100644 --- a/lib/class/settingstate.dart +++ b/lib/class/settingstate.dart @@ -6,16 +6,17 @@ import 'package:tsacdop/local_storage/key_value_storage.dart'; class SettingState extends ChangeNotifier { KeyValueStorage themestorage = KeyValueStorage('themes'); KeyValueStorage accentstorage = KeyValueStorage('accents'); - bool _isLoading; - bool get isLoagding => _isLoading; + KeyValueStorage autoupdatestorage = KeyValueStorage('autoupdate'); Future initData() async { await _getTheme(); await _getAccentSetColor(); + await _getAutoUpdate(); } ThemeMode _theme; ThemeMode get theme => _theme; + set setTheme(ThemeMode mode) { _theme = mode; _saveTheme(); @@ -31,11 +32,20 @@ class SettingState extends ChangeNotifier { notifyListeners(); } + bool _autoUpdate; + bool get autoUpdate => _autoUpdate; + set autoUpdate(bool boo) { + _autoUpdate = boo; + _saveAutoUpdate(); + notifyListeners(); + } + @override void addListener(VoidCallback listener) { super.addListener(listener); _getTheme(); _getAccentSetColor(); + _getAutoUpdate(); } _getTheme() async { @@ -62,6 +72,14 @@ class SettingState extends ChangeNotifier { _saveAccentSetColor() async { await accentstorage .saveString(_accentSetColor.toString().substring(10, 16)); - print(_accentSetColor.toString()); + } + + _getAutoUpdate() async { + int i = await autoupdatestorage.getInt(); + _autoUpdate = i == 0 ? false : true; + } + + _saveAutoUpdate() async { + await autoupdatestorage.saveInt(_autoUpdate ? 1 : 0); } } diff --git a/lib/class/sub_history.dart b/lib/class/sub_history.dart new file mode 100644 index 0000000..2a0291a --- /dev/null +++ b/lib/class/sub_history.dart @@ -0,0 +1,8 @@ +class SubHistory { + DateTime subDate; + DateTime delDate; + bool status; + String title; + String rssUrl; + SubHistory(this.status, this.delDate, this.subDate, this.rssUrl, this.title); +} diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index 0d5a768..5c73a80 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -10,6 +10,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:intl/intl.dart'; import 'package:tuple/tuple.dart'; +import 'package:audio_service/audio_service.dart'; import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; @@ -211,7 +212,7 @@ class _EpisodeDetailState extends State { ], ), ), - Selector( + Selector( selector: (_, audio) => audio.playerRunning, builder: (_, data, __) { return Container( @@ -290,7 +291,7 @@ class _MenuBarState extends State { @override Widget build(BuildContext context) { - var audio = Provider.of(context, listen: false); + var audio = Provider.of(context, listen: false); return Container( height: 50.0, decoration: BoxDecoration( @@ -346,7 +347,7 @@ class _MenuBarState extends State { ], ), DownloadButton(episodeBrief: widget.episodeItem), - Selector>( + Selector>( selector: (_, audio) => audio.queue.playlist.map((e) => e.enclosureUrl).toList(), builder: (_, data, __) { @@ -367,9 +368,9 @@ class _MenuBarState extends State { ), Spacer(), // Text(audio.audioState.toString()), - Selector>( + Selector>( selector: (_, audio) => - Tuple2(audio.episode, audio.backgroundAudioPlaying), + Tuple2(audio.episode, audio.audioState), builder: (_, data, __) { return (widget.episodeItem.title != data.item1?.title) ? Material( @@ -400,7 +401,7 @@ class _MenuBarState extends State { ), ) : (widget.episodeItem.title == data.item1?.title && - data.item2 == true) + data.item2 == BasicPlaybackState.playing) ? Container( padding: EdgeInsets.only(right: 30), child: SizedBox( @@ -424,9 +425,10 @@ class _MenuBarState extends State { class LinePainter extends CustomPainter { double _fraction; Paint _paint; - LinePainter(this._fraction) { + Color _maincolor; + LinePainter(this._fraction, this._maincolor) { _paint = Paint() - ..color = Colors.blue + ..color = _maincolor ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round; } @@ -483,14 +485,15 @@ class _LineLoaderState extends State @override Widget build(BuildContext context) { - return CustomPaint(painter: LinePainter(_fraction)); + return CustomPaint(painter: LinePainter(_fraction, Theme.of(context).accentColor)); } } class WavePainter extends CustomPainter { double _fraction; double _value; - WavePainter(this._fraction); + Color _color; + WavePainter(this._fraction, this._color); @override void paint(Canvas canvas, Size size) { if (_fraction < 0.5) { @@ -500,7 +503,7 @@ class WavePainter extends CustomPainter { } Path _path = Path(); Paint _paint = Paint() - ..color = Colors.blue + ..color = _color ..strokeWidth = 2.0 ..strokeCap = StrokeCap.round ..style = PaintingStyle.stroke; @@ -575,7 +578,7 @@ class _WaveLoaderState extends State @override Widget build(BuildContext context) { - return CustomPaint(painter: WavePainter(_fraction)); + return CustomPaint(painter: WavePainter(_fraction, Theme.of(context).accentColor)); } } diff --git a/lib/episodes/episodedownload.dart b/lib/episodes/episodedownload.dart index 9b4271e..5566093 100644 --- a/lib/episodes/episodedownload.dart +++ b/lib/episodes/episodedownload.dart @@ -1,14 +1,17 @@ import 'dart:isolate'; import 'dart:ui'; import 'dart:io'; +import 'dart:async'; import 'package:flutter/material.dart'; -import 'dart:async'; +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:tsacdop/class/episodebrief.dart'; +import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; class DownloadButton extends StatefulWidget { @@ -74,6 +77,7 @@ class _DownloadButtonState extends State { }); } + void _unbindBackgroundIsolate() { IsolateNameServer.removePortNameMapping('downloader_send_port'); } @@ -96,6 +100,7 @@ class _DownloadButtonState extends State { openFileFromNotification: false, ); var dbHelper = DBHelper(); + await dbHelper.saveDownloaded(task.link, task.taskId); Fluttertoast.showToast( msg: 'Downloading', @@ -103,6 +108,7 @@ class _DownloadButtonState extends State { ); } + void _deleteDownload(_TaskInfo task) async { await FlutterDownloader.remove( taskId: task.taskId, shouldDeleteContent: true); @@ -146,6 +152,16 @@ class _DownloadButtonState extends State { ); } + _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); + await Provider.of(context, listen: false).updateMediaItem(episode); + } + Future _prepare() async { final tasks = await FlutterDownloader.loadTasks(); @@ -161,7 +177,7 @@ class _DownloadButtonState extends State { } }); - _localPath = (await _getPath()) + '/' + widget.episodeBrief.feedTitle; + _localPath = path.join((await _getPath()) ,widget.episodeBrief.feedTitle); print(_localPath); final saveDir = Directory(_localPath); bool hasExisted = await saveDir.exists(); @@ -173,6 +189,8 @@ class _DownloadButtonState extends State { }); } + + Future _checkPermmison() async { PermissionStatus permission = await PermissionHandler() .checkPermissionStatus(PermissionGroup.storage); @@ -284,6 +302,7 @@ class _DownloadButtonState extends State { ), ); } else if (task.status == DownloadTaskStatus.complete) { + _saveMediaId(task); return _buttonOnMenu( Icon( Icons.done_all, @@ -307,4 +326,4 @@ class _TaskInfo { DownloadTaskStatus status = DownloadTaskStatus.undefined; _TaskInfo({this.name, this.link}); -} +} \ No newline at end of file diff --git a/lib/home/appbar/about.dart b/lib/home/appbar/about.dart index 45b511f..05fb01b 100644 --- a/lib/home/appbar/about.dart +++ b/lib/home/appbar/about.dart @@ -1,8 +1,7 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:font_awesome_flutter/font_awesome_flutter.dart'; - +import 'package:line_icons/line_icons.dart'; class AboutApp extends StatelessWidget { _launchUrl(String url) async { if (await canLaunch(url)) { @@ -71,7 +70,7 @@ class AboutApp extends StatelessWidget { image: AssetImage('assets/logo.png'), height: 80, ), - Text('Version: 0.1.1'), + Text('Version: 0.1.2'), ], ), ), @@ -79,7 +78,7 @@ class AboutApp extends StatelessWidget { padding: EdgeInsets.symmetric(horizontal: 50), height: 50, child: Text( - 'Tsacdop is a podcast client developed with flutter, a simple, beautiful, and easy-use player.', + 'Tsacdop is a podcasts client developed influtter, a simple, beautiful, and easy-use player.', textAlign: TextAlign.center, ), ), @@ -111,17 +110,17 @@ class AboutApp extends StatelessWidget { _listItem( context, 'GitHub', - FontAwesomeIcons.githubSquare, - 'https://github.com/stonaga/tsacdop'), + LineIcons.github, + 'https://github.com/stonaga/'), _listItem( context, 'Twitter', - FontAwesomeIcons.twitterSquare, + LineIcons.twitter, 'https://twitter.com'), _listItem( context, - 'Gmail', - FontAwesomeIcons.envelopeSquare, + 'Stone Gate', + LineIcons.hat_cowboy_solid, 'mailto:?subject=Tsacdop Feedback'), ], ), diff --git a/lib/home/appbar/addpodcast.dart b/lib/home/appbar/addpodcast.dart index 6319f67..7a582fe 100644 --- a/lib/home/appbar/addpodcast.dart +++ b/lib/home/appbar/addpodcast.dart @@ -36,6 +36,7 @@ class _MyHomePageState extends State { return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Theme.of(context).accentColorBrightness, + systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness, systemNavigationBarColor: Theme.of(context).primaryColor, statusBarColor: Theme.of(context).primaryColor, ), diff --git a/lib/home/appbar/popupmenu.dart b/lib/home/appbar/popupmenu.dart index 0953f57..20851d2 100644 --- a/lib/home/appbar/popupmenu.dart +++ b/lib/home/appbar/popupmenu.dart @@ -13,6 +13,7 @@ import 'package:color_thief_flutter/color_thief_flutter.dart'; import 'package:image/image.dart' as img; import 'package:uuid/uuid.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:line_icons/line_icons.dart'; import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/settings/settting.dart'; @@ -142,7 +143,7 @@ class PopupMenu extends StatelessWidget { void _saveOmpl(String path) async { File file = File(path); - String opml = file.readAsStringSync(); + try{String opml = file.readAsStringSync(); var content = xml.parse(opml); var total = content @@ -167,6 +168,15 @@ class PopupMenu extends StatelessWidget { } } print('Import fisnished'); + }} + catch(e){ + print(e); + Fluttertoast.showToast( + msg: 'File error, Subscribe failed', + gravity: ToastGravity.TOP, + ); + await Future.delayed(Duration(seconds: 5)); + importOmpl.importState = ImportState.stop; } } @@ -195,7 +205,7 @@ class PopupMenu extends StatelessWidget { padding: EdgeInsets.only(left: 10), child: Row( children: [ - Icon(Icons.refresh), + Icon(LineIcons.cloud_download_alt_solid), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Text('Refresh All'), ], @@ -208,7 +218,7 @@ class PopupMenu extends StatelessWidget { padding: EdgeInsets.only(left: 10), child: Row( children: [ - Icon(Icons.attachment), + Icon(LineIcons.paperclip_solid), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Text('Import OMPL'), ], @@ -226,7 +236,7 @@ class PopupMenu extends StatelessWidget { padding: EdgeInsets.only(left: 10), child: Row( children: [ - Icon(Icons.swap_calls), + Icon(LineIcons.cog_solid), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Text('Settings'), ], @@ -239,7 +249,7 @@ class PopupMenu extends StatelessWidget { padding: EdgeInsets.only(left: 10), child: Row( children: [ - Icon(Icons.info_outline), + Icon(LineIcons.info_circle_solid), Padding(padding: EdgeInsets.symmetric(horizontal: 5.0),), Text('About'), ], diff --git a/lib/home/audioplayer.dart b/lib/home/audioplayer.dart index 99d6108..f1bd414 100644 --- a/lib/home/audioplayer.dart +++ b/lib/home/audioplayer.dart @@ -7,6 +7,7 @@ import 'package:provider/provider.dart'; import 'package:marquee/marquee.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:tuple/tuple.dart'; +import 'package:audio_service/audio_service.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/audiostate.dart'; @@ -15,6 +16,97 @@ import 'package:tsacdop/home/audiopanel.dart'; import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/util/colorize.dart'; +class MyRoundSliderThumpShape extends SliderComponentShape { + const MyRoundSliderThumpShape({ + this.enabledThumbRadius = 10.0, + this.disabledThumbRadius, + this.thumbCenterColor, + }); + final Color thumbCenterColor; + + /// The preferred radius of the round thumb shape when the slider is enabled. + /// + /// If it is not provided, then the material default of 10 is used. + final double enabledThumbRadius; + + /// The preferred radius of the round thumb shape when the slider is disabled. + /// + /// If no disabledRadius is provided, then it is equal to the + /// [enabledThumbRadius] + final double disabledThumbRadius; + double get _disabledThumbRadius => disabledThumbRadius ?? enabledThumbRadius; + + @override + Size getPreferredSize(bool isEnabled, bool isDiscrete) { + return Size.fromRadius( + isEnabled == true ? enabledThumbRadius : _disabledThumbRadius); + } + + @override + void paint( + PaintingContext context, + Offset center, { + Animation activationAnimation, + @required Animation enableAnimation, + bool isDiscrete, + TextPainter labelPainter, + RenderBox parentBox, + @required SliderThemeData sliderTheme, + TextDirection textDirection, + double value, + }) { + assert(context != null); + assert(center != null); + assert(enableAnimation != null); + assert(sliderTheme != null); + assert(sliderTheme.disabledThumbColor != null); + assert(sliderTheme.thumbColor != null); + + final Canvas canvas = context.canvas; + final Tween radiusTween = Tween( + begin: _disabledThumbRadius, + end: enabledThumbRadius, + ); + final ColorTween colorTween = ColorTween( + begin: sliderTheme.disabledThumbColor, + end: sliderTheme.thumbColor, + ); + + canvas.drawCircle( + center, + radiusTween.evaluate(enableAnimation), + Paint() + ..color = thumbCenterColor + ..style = PaintingStyle.stroke + ..strokeWidth = 2, + ); + canvas.drawLine( + Offset(center.dx - 6, center.dy), + Offset(center.dx + 6, center.dy), + Paint() + ..color = Colors.grey[300] + ..style = PaintingStyle.fill + ..strokeWidth = 2, + ); + canvas.drawCircle( + center, + radiusTween.evaluate(enableAnimation) - 2, + Paint() + ..color = colorTween.evaluate(enableAnimation) + ..style = PaintingStyle.fill + ..strokeWidth = 2, + ); + canvas.drawLine( + Offset(center.dx - 5, center.dy - 2), + Offset(center.dx + 5, center.dy + 2), + Paint() + ..color = Colors.transparent + ..style = PaintingStyle.fill + ..strokeWidth = 2, + ); + } +} + class PlayerWidget extends StatefulWidget { @override _PlayerWidgetState createState() => _PlayerWidgetState(); @@ -50,17 +142,17 @@ class _PlayerWidgetState extends State { _timeLeft = _minSelected; _timer = Timer.periodic(Duration(minutes: 1), (timer) { setState(() { - if(_timeLeft < 1){ + if (_timeLeft < 1) { _timer.cancel(); - } else{ + } else { _timeLeft = _timeLeft - 1; } }); - }); + }); } Widget _sleepTimer(BuildContext context) { - var audio = Provider.of(context); + var audio = Provider.of(context); return Container( height: 50, margin: EdgeInsets.all(10.0), @@ -141,7 +233,7 @@ class _PlayerWidgetState extends State { } Widget _expandedPanel(BuildContext context) { - var audio = Provider.of(context, listen: false); + var audio = Provider.of(context, listen: false); return Stack( children: [ Container( @@ -156,7 +248,7 @@ class _PlayerWidgetState extends State { height: 80.0, padding: EdgeInsets.all(20), alignment: Alignment.center, - child: Selector( + child: Selector( selector: (_, audio) => audio.episode.title, builder: (_, title, __) { return Container( @@ -201,8 +293,12 @@ class _PlayerWidgetState extends State { }, ), ), - Consumer( + Consumer( builder: (_, data, __) { + Color _c = + (Theme.of(context).brightness == Brightness.light) + ? data.episode.primaryColor.colorizedark() + : data.episode.primaryColor.colorizeLight(); return Column( mainAxisAlignment: MainAxisAlignment.start, mainAxisSize: MainAxisSize.min, @@ -215,10 +311,12 @@ class _PlayerWidgetState extends State { .accentColor .withOpacity(0.5), inactiveTrackColor: Colors.grey[300], - trackHeight: 3.0, + trackHeight: 2.0, thumbColor: Theme.of(context).accentColor, - thumbShape: RoundSliderThumbShape( - enabledThumbRadius: 6.0), + thumbShape: MyRoundSliderThumpShape( + enabledThumbRadius: 5.0, + disabledThumbRadius: 5.0, + thumbCenterColor: _c), overlayColor: Theme.of(context).accentColor.withAlpha(32), overlayShape: @@ -238,7 +336,7 @@ class _PlayerWidgetState extends State { children: [ Text( _stringForSeconds( - data.backgroundAudioPosition) ?? + data.backgroundAudioPosition / 1000) ?? '', style: TextStyle(fontSize: 10), ), @@ -250,7 +348,12 @@ class _PlayerWidgetState extends State { style: const TextStyle( color: const Color(0xFFFF0000))) : Text( - data.remoteAudioLoading + data.audioState == + BasicPlaybackState + .buffering || + data.audioState == + BasicPlaybackState + .connecting ? 'Buffring...' : '', style: TextStyle( @@ -261,7 +364,7 @@ class _PlayerWidgetState extends State { ), Text( _stringForSeconds( - data.backgroundAudioDuration) ?? + data.backgroundAudioDuration / 1000) ?? '', style: TextStyle(fontSize: 10), ), @@ -274,8 +377,8 @@ class _PlayerWidgetState extends State { ), Container( height: 100, - child: Selector( - selector: (_, audio) => audio.backgroundAudioPlaying, + child: Selector( + selector: (_, audio) => audio.audioState, builder: (_, backplay, __) { return Material( color: Colors.transparent, @@ -285,22 +388,24 @@ class _PlayerWidgetState extends State { children: [ IconButton( padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? () => audio.forwardAudio(-10) - : null, + onPressed: + backplay == BasicPlaybackState.playing + ? () => audio.forwardAudio(-10) + : null, iconSize: 32.0, icon: Icon(Icons.replay_10), color: Theme.of(context).tabBarTheme.labelColor), - backplay + backplay == BasicPlaybackState.playing ? IconButton( padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? () { - audio.pauseAduio(); - } - : null, + onPressed: + backplay == BasicPlaybackState.playing + ? () { + audio.pauseAduio(); + } + : null, iconSize: 40.0, icon: Icon(Icons.pause_circle_filled), color: Theme.of(context) @@ -309,11 +414,12 @@ class _PlayerWidgetState extends State { : IconButton( padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? null - : () { - audio.resumeAudio(); - }, + onPressed: + backplay == BasicPlaybackState.playing + ? null + : () { + audio.resumeAudio(); + }, iconSize: 40.0, icon: Icon(Icons.play_circle_filled), color: Theme.of(context) @@ -321,9 +427,10 @@ class _PlayerWidgetState extends State { .labelColor), IconButton( padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? () => audio.forwardAudio(30) - : null, + onPressed: + backplay == BasicPlaybackState.playing + ? () => audio.forwardAudio(30) + : null, iconSize: 32.0, icon: Icon(Icons.forward_30), color: @@ -344,7 +451,7 @@ class _PlayerWidgetState extends State { color: Theme.of(context).scaffoldBackgroundColor, borderRadius: BorderRadius.all(Radius.circular(10.0)), ), - child: Selector>( selector: (_, audio) => Tuple3(audio.episode, audio.stopOnComplete, audio.showStopWatch), @@ -394,6 +501,7 @@ class _PlayerWidgetState extends State { ], onSelected: (value) { if (value == 1) { + audio.sleepTimer(_minSelected); audio.setStopOnComplete = true; } else if (value == 2) { setState(() => _showTimer = true); @@ -414,8 +522,7 @@ class _PlayerWidgetState extends State { color: Theme.of(context).accentColor, ), - child: Text( - _timeLeft.toString(), + child: Text(_timeLeft.toString(), style: TextStyle( color: Colors.white)), ), @@ -475,7 +582,7 @@ class _PlayerWidgetState extends State { // margin: EdgeInsets.all(20), //padding: EdgeInsets.only(bottom: 10.0), decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(10.0)), + // borderRadius: BorderRadius.all(Radius.circular(10.0)), color: Theme.of(context).scaffoldBackgroundColor, ), child: Column( @@ -511,7 +618,7 @@ class _PlayerWidgetState extends State { ), ), Expanded( - child: Selector>( + child: Selector>( selector: (_, audio) => audio.queue.playlist, builder: (_, playlist, __) { return ListView.builder( @@ -623,7 +730,7 @@ class _PlayerWidgetState extends State { } Widget _miniPanel(double width, BuildContext context) { - var audio = Provider.of(context, listen: false); + var audio = Provider.of(context, listen: false); return Container( decoration: BoxDecoration( color: Theme.of(context).primaryColor, @@ -631,7 +738,7 @@ class _PlayerWidgetState extends State { height: 60, child: Column(mainAxisAlignment: MainAxisAlignment.start, children: [ - Selector>( + Selector>( selector: (_, audio) => Tuple2(audio.episode?.primaryColor, audio.seekSliderValue), builder: (_, data, __) { @@ -657,58 +764,33 @@ class _PlayerWidgetState extends State { children: [ Expanded( flex: 4, - child: Selector( + child: Selector( selector: (_, audio) => audio.episode.title, builder: (_, title, __) { - return LayoutBuilder( - builder: (context, size) { - var span = TextSpan( - text: title, - style: TextStyle(fontWeight: FontWeight.bold), - ); - var tp = TextPainter( - text: span, - maxLines: 2, - textDirection: TextDirection.ltr); - tp.layout(maxWidth: size.maxWidth); - if (tp.didExceedMaxLines) { - return Marquee( - text: title, - style: TextStyle(fontWeight: FontWeight.bold), - scrollAxis: Axis.vertical, - crossAxisAlignment: CrossAxisAlignment.start, - blankSpace: 30.0, - velocity: 50.0, - pauseAfterRound: Duration(seconds: 1), - startPadding: 30.0, - accelerationDuration: Duration(seconds: 1), - accelerationCurve: Curves.linear, - decelerationDuration: Duration(milliseconds: 500), - decelerationCurve: Curves.easeOut, - ); - } else { - return Text( - title, - style: TextStyle(fontWeight: FontWeight.bold), - ); - } - }, + return Text( + title, + style: TextStyle(fontWeight: FontWeight.bold), + maxLines: 2, + overflow: TextOverflow.clip, ); }, ), ), Expanded( flex: 2, - child: Selector>( + child: Selector>( selector: (_, audio) => Tuple2( - audio.remoteAudioLoading, + audio.audioState, (audio.backgroundAudioDuration - - audio.backgroundAudioPosition)), + audio.backgroundAudioPosition) / + 1000), builder: (_, data, __) { return Container( padding: EdgeInsets.symmetric(horizontal: 10), alignment: Alignment.center, - child: data.item1 + child: data.item1 == BasicPlaybackState.buffering || + data.item1 == BasicPlaybackState.connecting ? Text( 'Buffring...', style: TextStyle( @@ -730,30 +812,32 @@ class _PlayerWidgetState extends State { ), Expanded( flex: 2, - child: Selector( - selector: (_, audio) => audio.backgroundAudioPlaying, + child: Selector( + selector: (_, audio) => audio.audioState, builder: (_, audioplay, __) { return Row( mainAxisSize: MainAxisSize.min, mainAxisAlignment: MainAxisAlignment.spaceEvenly, children: [ - audioplay + audioplay == BasicPlaybackState.playing ? InkWell( - onTap: audioplay - ? () { - audio.pauseAduio(); - } - : null, + onTap: + audioplay == BasicPlaybackState.playing + ? () { + audio.pauseAduio(); + } + : null, child: ImageRotate( title: audio.episode.title, path: audio.episode.imagePath), ) : InkWell( - onTap: audioplay - ? null - : () { - audio.resumeAudio(); - }, + onTap: + audioplay == BasicPlaybackState.playing + ? null + : () { + audio.resumeAudio(); + }, child: Stack( alignment: Alignment.center, children: [ @@ -781,11 +865,10 @@ class _PlayerWidgetState extends State { ), ), IconButton( - onPressed: audioplay - ? () => audio.forwardAudio(30) - : null, + onPressed: + () => audio.playNext(), iconSize: 25.0, - icon: Icon(Icons.forward_30), + icon: Icon(Icons.skip_next), color: Theme.of(context).tabBarTheme.labelColor), ], @@ -803,7 +886,7 @@ class _PlayerWidgetState extends State { @override Widget build(BuildContext context) { double _width = MediaQuery.of(context).size.width; - return Selector( + return Selector( selector: (_, audio) => audio.playerRunning, builder: (_, playerrunning, __) { return !playerrunning diff --git a/lib/home/home.dart b/lib/home/home.dart index 0cd1b39..0fcfaf4 100644 --- a/lib/home/home.dart +++ b/lib/home/home.dart @@ -31,7 +31,7 @@ class _HomeState extends State { } _getPlaylist() async { - await Provider.of(context, listen: false).loadPlaylist(); + await Provider.of(context, listen: false).loadPlaylist(); setState(() { _loadPlay = true; }); @@ -39,7 +39,7 @@ class _HomeState extends State { @override Widget build(BuildContext context) { - var audio = Provider.of(context, listen: false); + var audio = Provider.of(context, listen: false); return Stack(children: [ Column( mainAxisAlignment: MainAxisAlignment.start, @@ -58,7 +58,7 @@ class _HomeState extends State { bottom: 50, right: _loadPlay ? 5 : -25, child: Container( - child: Selector>( + child: Selector>( selector: (_, audio) => Tuple3(audio.playerRunning, audio.queue, audio.lastPositin), builder: (_, data, __) => !_loadPlay @@ -90,7 +90,7 @@ class _HomeState extends State { offset: Offset(1, 1)), ]), height: 40, - child: Text(_stringForSeconds(data.item3) + '...', + child: Text(_stringForSeconds(data.item3~/1000) + '...', style: TextStyle(color: Colors.white)), ), CircleAvatar( diff --git a/lib/home/homescroll.dart b/lib/home/homescroll.dart index 0570783..fafd987 100644 --- a/lib/home/homescroll.dart +++ b/lib/home/homescroll.dart @@ -5,7 +5,9 @@ import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:fluttertoast/fluttertoast.dart'; - +import 'package:tsacdop/class/audiostate.dart'; +import 'package:tuple/tuple.dart'; +import 'package:line_icons/line_icons.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/class/importompl.dart'; import 'package:tsacdop/class/podcast_group.dart'; @@ -89,7 +91,9 @@ class _ScrollPodcastsState extends State { style: Theme.of(context) .textTheme .bodyText1 - .copyWith(color: Colors.red[300]), + .copyWith( + color: Theme.of(context) + .accentColor), )), Spacer(), Container( @@ -184,7 +188,9 @@ class _ScrollPodcastsState extends State { style: Theme.of(context) .textTheme .bodyText1 - .copyWith(color: Colors.red[300]), + .copyWith( + color: Theme.of(context) + .accentColor), )), Spacer(), Container( @@ -374,43 +380,78 @@ class ShowEpisode extends StatelessWidget { final List podcast; final PodcastLocal podcastLocal; ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key); - + Offset offset; @override Widget build(BuildContext context) { double _width = MediaQuery.of(context).size.width; - _showPopupMenu(Offset offset) async { - print(offset.dx); + _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, + bool isPlaying, bool isInPlaylist) async { + var audio = Provider.of(context, listen: false); double left = offset.dx; double top = offset.dy; - await showMenu( + await showMenu( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))), context: context, position: RelativeRect.fromLTRB(left, top, _width - left, 0), - items: [ + items: >[ PopupMenuItem( + value: 0, child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, children: [ - Icon(Icons.play_circle_outline), - Padding(padding: EdgeInsets.symmetric(horizontal: 2),), - Text('Play') + Icon( + LineIcons.play_circle_solid, + color: Theme.of(context).accentColor, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + !isPlaying ? Text('Play') : Text('Playing'), ], ), ), - PopupMenuItem(child: Row( - children: [ - Icon(Icons.favorite_border), - Padding(padding: EdgeInsets.symmetric(horizontal: 2),), - Text('Like') - ], - )), + PopupMenuItem( + value: 1, + child: Row( + children: [ + Icon( + LineIcons.clock_solid, + color: Colors.red, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + !isInPlaylist ? Text('Later') : Text('Remove') + ], + )), ], - elevation: 8.0, - ); + elevation: 5.0, + ).then((value) { + if (value == 0) { + if (!isPlaying) audio.episodeLoad(episode); + } else if (value == 1) { + if (isInPlaylist) { + audio.addToPlaylist(episode); + Fluttertoast.showToast( + msg: 'Added to playlist', + gravity: ToastGravity.BOTTOM, + ); + } else { + audio.delFromPlaylist(episode); + Fluttertoast.showToast( + msg: 'Removed from playlist', + gravity: ToastGravity.BOTTOM, + ); + } + } + }); } return CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), + // physics: const AlwaysScrollableScrollPhysics(), + physics: ClampingScrollPhysics(), primary: false, slivers: [ SliverPadding( @@ -427,88 +468,110 @@ class ShowEpisode extends StatelessWidget { Color _c = (Theme.of(context).brightness == Brightness.light) ? podcastLocal.primaryColor.colorizedark() : podcastLocal.primaryColor.colorizeLight(); - return GestureDetector( - onLongPressStart: (details) => _showPopupMenu(Offset( - details.globalPosition.dx, details.globalPosition.dy)), - onTap: () { - Navigator.push( - context, - ScaleRoute( - page: EpisodeDetail( - episodeItem: podcast[index], - heroTag: 'scroll', - //unique hero tag - )), - ); - }, - child: Container( + return Selector>>( + selector: (_, audio) => Tuple2( + audio?.episode, + audio.queue.playlist.map((e) => e.enclosureUrl).toList(), + ), + builder: (_, data, __) => Container( decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(5.0)), color: Theme.of(context).scaffoldBackgroundColor, - border: Border.all( - color: Theme.of(context).brightness == Brightness.light - ? Theme.of(context).primaryColor - : Theme.of(context).scaffoldBackgroundColor, - // color: Theme.of(context).primaryColor, - width: 3.0, - ), ), alignment: Alignment.center, - padding: EdgeInsets.all(10.0), - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Expanded( - flex: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, + child: Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + onTapDown: (details) => offset = Offset( + details.globalPosition.dx, + details.globalPosition.dy), + onLongPress: () => _showPopupMenu( + offset, + podcast[index], + context, + data.item1 == podcast[index], + data.item2.contains(podcast[index].enclosureUrl)), + onTap: () { + Navigator.push( + context, + ScaleRoute( + page: EpisodeDetail( + episodeItem: podcast[index], + heroTag: 'scroll', + //unique hero tag + )), + ); + }, + child: Container( + // decoration: BoxDecoration( + // border: Border.all( + // color: Theme.of(context).brightness == + // Brightness.light + // ? Theme.of(context).primaryColor + // : Theme.of(context).scaffoldBackgroundColor, + // width: 0.0, + // ), + // ), + padding: EdgeInsets.all(10.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Hero( - tag: podcast[index].enclosureUrl + 'scroll', + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Hero( + tag: podcast[index].enclosureUrl + + 'scroll', + child: Container( + height: _width / 18, + width: _width / 18, + child: CircleAvatar( + backgroundImage: FileImage(File( + "${podcastLocal.imagePath}")), + ), + ), + ), + Spacer(), + ], + ), + ), + Expanded( + flex: 5, child: Container( - height: _width / 18, - width: _width / 18, - child: CircleAvatar( - backgroundImage: FileImage( - File("${podcastLocal.imagePath}")), + padding: EdgeInsets.only(top: 2.0), + alignment: Alignment.topLeft, + child: Text( + podcast[index].title, + style: TextStyle( + fontSize: _width / 32, + ), + maxLines: 4, + overflow: TextOverflow.fade, ), ), ), - Spacer(), + Expanded( + flex: 1, + child: Container( + alignment: Alignment.bottomLeft, + child: Text( + podcast[index].dateToString(), + //podcast[index].pubDate.substring(4, 16), + style: TextStyle( + fontSize: _width / 35, + color: _c, + fontStyle: FontStyle.italic, + ), + ), + )), ], ), ), - Expanded( - flex: 5, - child: Container( - padding: EdgeInsets.only(top: 2.0), - alignment: Alignment.topLeft, - child: Text( - podcast[index].title, - style: TextStyle( - fontSize: _width / 32, - ), - maxLines: 4, - overflow: TextOverflow.fade, - ), - ), - ), - Expanded( - flex: 1, - child: Container( - alignment: Alignment.bottomLeft, - child: Text( - podcast[index].dateToString(), - //podcast[index].pubDate.substring(4, 16), - style: TextStyle( - fontSize: _width / 35, - color: _c, - fontStyle: FontStyle.italic, - ), - ), - ), - ), - ], + ), ), ), ); diff --git a/lib/home/hometab.dart b/lib/home/hometab.dart index d61cec2..e2aed7b 100644 --- a/lib/home/hometab.dart +++ b/lib/home/hometab.dart @@ -2,7 +2,7 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:tsacdop/class/episodebrief.dart'; -import 'package:tsacdop/home/paly_history.dart'; +import 'package:tsacdop/settings/history.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/util/episodegrid.dart'; @@ -49,8 +49,8 @@ class _MainTabState extends State with TickerProviderStateMixin { ], onSelected: (value) { if (value == 0) { - Navigator.push( - context, MaterialPageRoute(builder: (context) => PlayedHistory())); + Navigator.push(context, + MaterialPageRoute(builder: (context) => PlayedHistory())); } }, ); @@ -81,6 +81,7 @@ class _MainTabState extends State with TickerProviderStateMixin { height: 50, alignment: Alignment.centerLeft, child: TabBar( + indicatorSize: TabBarIndicatorSize.tab, isScrollable: true, labelPadding: EdgeInsets.all(10.0), controller: _controller, @@ -134,26 +135,73 @@ class RecentUpdate extends StatefulWidget { } class _RecentUpdateState extends State { - Future> _getRssItem() async { + Future> _getRssItem(int top) async { var dbHelper = DBHelper(); - List episodes = await dbHelper.getRecentRssItem(); + List episodes = await dbHelper.getRecentRssItem(top); return episodes; } + ScrollController _controller; + int _top; + bool _loadMore; + _scrollListener() async { + if (_controller.offset == _controller.position.maxScrollExtent) { + if (mounted) setState(() => _loadMore = true); + await Future.delayed(Duration(seconds: 3)); + if (mounted) + setState(() { + _top = _top + 33; + _loadMore = false; + }); + } + } + + @override + void initState() { + super.initState(); + _loadMore = false; + _top = 33; + _controller = ScrollController(); + _controller.addListener(_scrollListener); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + @override Widget build(BuildContext context) { return FutureBuilder>( - future: _getRssItem(), + future: _getRssItem(_top), builder: (context, snapshot) { if (snapshot.hasError) print(snapshot.error); return (snapshot.hasData) - ? EpisodeGrid( - podcast: snapshot.data, - showDownload: false, - showFavorite: false, - showNumber: false, - heroTag: 'recent', - ) + ? CustomScrollView( + controller: _controller, + physics: const AlwaysScrollableScrollPhysics(), + primary: false, + slivers: [ + EpisodeGrid( + podcast: snapshot.data, + showDownload: false, + showFavorite: false, + showNumber: false, + heroTag: 'recent', + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return _loadMore + ? Container( + height: 2, child: LinearProgressIndicator()) + : Center(); + }, + childCount: 1, + ), + ), + ]) : Center(child: CircularProgressIndicator()); }, ); @@ -179,12 +227,18 @@ class _MyFavoriteState extends State { builder: (context, snapshot) { if (snapshot.hasError) print(snapshot.error); return (snapshot.hasData) - ? EpisodeGrid( - podcast: snapshot.data, - showDownload: false, - showFavorite: false, - showNumber: false, - heroTag: 'favorite', + ? CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + primary: false, + slivers: [ + EpisodeGrid( + podcast: snapshot.data, + showDownload: false, + showFavorite: false, + showNumber: false, + heroTag: 'favorite', + ) + ], ) : Center(child: CircularProgressIndicator()); }, @@ -211,12 +265,19 @@ class _MyDownloadState extends State { builder: (context, snapshot) { if (snapshot.hasError) print(snapshot.error); return (snapshot.hasData) - ? EpisodeGrid( - podcast: snapshot.data, - showDownload: true, - showFavorite: false, - showNumber: false, - heroTag: 'download', + ? CustomScrollView( + physics: const AlwaysScrollableScrollPhysics(), + primary: false, + slivers: [ + EpisodeGrid( + podcast: snapshot.data, + showDownload: true, + showFavorite: false, + showNumber: false, + heroTag: 'download', + ) + ], + ) : Center(child: CircularProgressIndicator()); }, diff --git a/lib/home/paly_history.dart b/lib/home/paly_history.dart deleted file mode 100644 index 1a33c5f..0000000 --- a/lib/home/paly_history.dart +++ /dev/null @@ -1,73 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; -import 'package:tsacdop/class/audiostate.dart'; - -class PlayedHistory extends StatefulWidget{ - @override - _PlayedHistoryState createState() => _PlayedHistoryState(); -} - -class _PlayedHistoryState extends State { - - Future> gerPlayHistory() async{ - DBHelper dbHelper = DBHelper(); - List playHistory; - playHistory = await dbHelper.getPlayHistory(); - await Future.forEach(playHistory, (playHistory) async{ - await playHistory.getEpisode(); - }); - return playHistory; - } - static String _stringForSeconds(double seconds) { - if (seconds == null) return null; - return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; - } - - @override - Widget build(BuildContext context) { - return AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarIconBrightness: Theme.of(context).accentColorBrightness, - systemNavigationBarColor: Theme.of(context).primaryColor, - statusBarColor: Theme.of(context).primaryColor, - ), - child: SafeArea( - child: Scaffold( - appBar: AppBar( - title: Text('History'), - centerTitle: true, - elevation: 0, - backgroundColor: Theme.of(context).primaryColor, - ), - body: FutureBuilder>( - future: gerPlayHistory(), - builder: (context, snapshot) { - return - snapshot.hasData ? - ListView.builder( - shrinkWrap: true, - scrollDirection: Axis.vertical, - itemCount: snapshot.data.length, - itemBuilder: (BuildContext context, int index){ - return Column( - children: [ - ListTile( - title: Text(snapshot.data[index].title), - subtitle: Text(_stringForSeconds(snapshot.data[index].seconds)), - ), - Divider(height: 2), - ], - ); - } - ) - : Center( - child: CircularProgressIndicator(), - ); - }, - ), - ), - ), - ); - } -} \ No newline at end of file diff --git a/lib/local_storage/key_value_storage.dart b/lib/local_storage/key_value_storage.dart index 2911626..6ea8ef8 100644 --- a/lib/local_storage/key_value_storage.dart +++ b/lib/local_storage/key_value_storage.dart @@ -43,7 +43,7 @@ class KeyValueStorage { return prefs.getInt(key); } - Future saveStringlist(List playList) async{ + Future saveStringList(List playList) async{ SharedPreferences prefs = await SharedPreferences.getInstance(); return prefs.setStringList(key, playList); } diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 83858f3..5bae211 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -8,6 +8,7 @@ import 'package:tsacdop/class/podcastlocal.dart'; import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/webfeed/webfeed.dart'; +import 'package:tsacdop/class/sub_history.dart'; class DBHelper { static Database _db; @@ -35,10 +36,13 @@ class DBHelper { enclosure_url TEXT UNIQUE, enclosure_length INTEGER, pubDate TEXT, description TEXT, feed_id TEXT, feed_link TEXT, milliseconds INTEGER, duration INTEGER DEFAULT 0, explicit INTEGER DEFAULT 0, liked INTEGER DEFAULT 0, - downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0)"""); + downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT)"""); await db.execute( """CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE, seconds REAL, seek_value REAL, add_date INTEGER)"""); + await db.execute( + """CREATE TABLE SubscribeHistory(id TEXT PRIMARY KEY, title TEXT, rss_url TEXT UNIQUE, + add_date INTEGER, remove_date INTEGER DEFAULT 0, status INTEGER DEFAULT 0)"""); } Future> getPodcastLocal(List podcasts) async { @@ -97,7 +101,7 @@ class DBHelper { int _milliseconds = DateTime.now().millisecondsSinceEpoch; var dbClient = await database; await dbClient.transaction((txn) async { - return await txn.rawInsert( + await txn.rawInsert( """INSERT OR IGNORE INTO PodcastLocal (id, title, imageUrl, rssUrl, primaryColor, author, description, add_date, imagePath, provider, link) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", [ @@ -114,6 +118,16 @@ class DBHelper { podcastLocal.link ]); }); + await dbClient.transaction((txn) async { + await txn.rawInsert( + """REPLACE INTO SubscribeHistory(id, title, rss_url, add_date) VALUES (?, ?, ?, ?)""", + [ + podcastLocal.id, + podcastLocal.title, + podcastLocal.rssUrl, + _milliseconds + ]); + }); } Future saveFiresideData(List list) async { @@ -146,6 +160,10 @@ class DBHelper { print('Removed all download tasks'); } await dbClient.rawDelete('DELETE FROM Episodes WHERE feed_id=?', [id]); + int _milliseconds = DateTime.now().millisecondsSinceEpoch; + await dbClient.rawUpdate( + """UPDATE SubscribeHistory SET remove_date = ? , status = ? WHERE id = ?""", + [_milliseconds, 1, id]); } Future saveHistory(PlayHistory history) async { @@ -174,16 +192,46 @@ class DBHelper { """); List playHistory = []; list.forEach((record) { - playHistory.add(PlayHistory( - record['title'], - record['enclosure_url'], - record['seconds'], - record['seek_value'], - )); + playHistory.add(PlayHistory(record['title'], record['enclosure_url'], + record['seconds'], record['seek_value'], + playdate: DateTime.fromMillisecondsSinceEpoch(record['add_date']))); }); return playHistory; } + Future> getSubHistory() async{ + var dbClient = await database; + List list = await dbClient.rawQuery( + """SELECT title, rss_url, add_date, remove_date, status FROM SubscribeHistory + ORDER BY add_date DESC""" + ); + return list.map((record) => SubHistory( + record['status']==0 ? true : false, DateTime.fromMillisecondsSinceEpoch(record['remove_date']), + DateTime.fromMillisecondsSinceEpoch(record['add_date']), record['rss_url'], record['title'] + )).toList(); + } + + Future listenMins(int day) async { + var dbClient = await database; + var now = DateTime.now(); + var start = DateTime(now.year, now.month, now.day) + .subtract(Duration(days: day)) + .millisecondsSinceEpoch; + var end = DateTime(now.year, now.month, now.day) + .subtract(Duration(days: (day - 1))) + .millisecondsSinceEpoch; + List list = await dbClient.rawQuery( + "SELECT seconds FROM PlayHistory WHERE add_date > ? AND add_date < ?", + [start, end]); + double sum = 0; + if (list.length == 0) { + sum = 0; + } else { + list.forEach((record) => sum += record['seconds']); + } + return (sum ~/ 60).toDouble(); + } + Future getPosition(EpisodeBrief episodeBrief) async { var dbClient = await database; List list = await dbClient.rawQuery( @@ -200,6 +248,17 @@ class DBHelper { RegExp hhmm = RegExp(r'[0-2][0-9]\:[0-5][0-9]'); RegExp ddmmm = RegExp(r'[0-3][0-9]\s[A-Z][a-z]{2}'); RegExp mmDd = RegExp(r'([0-1]|\s)[0-9]\-[0-3][0-9]'); + // RegExp timezone + RegExp z = RegExp(r'(\+|\-)[0-1][0-9]00'); + String timezone = z.stringMatch(pubDate); + int timezoneInt = 0; + if(timezone!=null){ + if(timezone.substring(0, 1) == '-'){ + timezoneInt = int.parse(timezone.substring(1,2)); + } else { + timezoneInt = -int.parse(timezone.substring(1,2)); + } + } try { date = DateFormat('EEE, dd MMM yyyy HH:mm:ss Z', 'en_US').parse(pubDate); } catch (e) { @@ -209,7 +268,7 @@ class DBHelper { try { date = DateFormat('EEE, dd MMM yyyy HH:mm Z', 'en_US').parse(pubDate); } catch (e) { - //parse date using regex, bug in parse maonth/day + //parse date using regex, still have issue in parse maonth/day String year = yyyy.stringMatch(pubDate); String time = hhmm.stringMatch(pubDate); String month = ddmmm.stringMatch(pubDate); @@ -221,12 +280,12 @@ class DBHelper { date = DateFormat('mm-dd yyyy HH:mm', 'en_US') .parse(month + ' ' + year + ' ' + time); } else { - date = DateTime.now(); + date = DateTime.now().toUtc(); } } } } - return date; + return date.add(Duration(hours: timezoneInt)); } int getExplicit(bool b) { @@ -276,7 +335,7 @@ class DBHelper { await dbClient.transaction((txn) { return txn.rawInsert( """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, - description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""", + description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", [ _title, _url, @@ -287,6 +346,7 @@ class DBHelper { _milliseconds, _duration, _explicit, + _url ]); }); } @@ -333,7 +393,7 @@ class DBHelper { await dbClient.transaction((txn) { return txn.rawInsert( """INSERT OR IGNORE INTO Episodes(title, enclosure_url, enclosure_length, pubDate, - description, feed_id, milliseconds, duration, explicit) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?)""", + description, feed_id, milliseconds, duration, explicit, media_id) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)""", [ _title, _url, @@ -344,6 +404,7 @@ class DBHelper { _milliseconds, _duration, _explicit, + _url ]); }); } @@ -358,7 +419,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.downloaded, P.primaryColor , E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id WHERE P.id = ? ORDER BY E.milliseconds DESC""", [id]); for (int x = 0; x < list.length; x++) { @@ -373,7 +434,8 @@ class DBHelper { list[x]['downloaded'], list[x]['duration'], list[x]['explicit'], - list[x]['imagePath'])); + list[x]['imagePath'], + list[x]['media_id'])); } return episodes; } @@ -384,7 +446,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.downloaded, P.primaryColor, E.media_id 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++) { @@ -399,7 +461,8 @@ class DBHelper { list[x]['downloaded'], list[x]['duration'], list[x]['explicit'], - list[x]['imagePath'])); + list[x]['imagePath'], + list[x]['media_id'])); } return episodes; } @@ -410,7 +473,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.downloaded, P.primaryColor, E.media_id 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]); @@ -427,19 +490,20 @@ class DBHelper { list.first['downloaded'], list.first['duration'], list.first['explicit'], - list.first['imagePath']); + list.first['imagePath'], + list.first['media_id']); return episode; } - Future> getRecentRssItem() async { + Future> getRecentRssItem(int top) async { var dbClient = await database; List episodes = List(); 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.downloaded, P.imagePath, P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id - ORDER BY E.milliseconds DESC LIMIT 99"""); + ORDER BY E.milliseconds DESC LIMIT ? """, [top]); for (int x = 0; x < list.length; x++) { episodes.add(EpisodeBrief( list[x]['title'], @@ -452,7 +516,8 @@ class DBHelper { list[x]['doanloaded'], list[x]['duration'], list[x]['explicit'], - list[x]['imagePath'])); + list[x]['imagePath'], + list[x]['media_id'])); } return episodes; } @@ -463,7 +528,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 FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + 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 99"""); for (int x = 0; x < list.length; x++) { episodes.add(EpisodeBrief( @@ -477,7 +542,8 @@ class DBHelper { list[x]['downloaded'], list[x]['duration'], list[x]['explicit'], - list[x]['imagePath'])); + list[x]['imagePath'], + list[x]['media_id'])); } return episodes; } @@ -507,10 +573,21 @@ class DBHelper { return count; } + + Future saveMediaId(String url, String path) async { + var dbClient = await database; + int _milliseconds = DateTime.now().millisecondsSinceEpoch; + int count = await dbClient.rawUpdate( + "UPDATE Episodes SET media_id = ?, download_date = ? WHERE enclosure_url = ?", + [path, _milliseconds, url]); + return count; + } + + Future delDownloaded(String url) async { var dbClient = await database; int count = await dbClient.rawUpdate( - "UPDATE Episodes SET downloaded = 'ND' WHERE enclosure_url = ?", [url]); + "UPDATE Episodes SET downloaded = 'ND', media_id = ? WHERE enclosure_url = ?", [url, url]); print('Deleted ' + url); return count; } @@ -521,7 +598,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 FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + 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"""); for (int x = 0; x < list.length; x++) { episodes.add(EpisodeBrief( @@ -535,7 +612,8 @@ class DBHelper { list[x]['downloaded'], list[x]['duration'], list[x]['explicit'], - list[x]['imagePath'])); + list[x]['imagePath'], + list[x]['media_id'])); } return episodes; } @@ -562,7 +640,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 FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + P.primaryColor, E.media_id FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id WHERE E.enclosure_url = ?""", [url]); episode = EpisodeBrief( list.first['title'], @@ -575,7 +653,33 @@ class DBHelper { list.first['downloaded'], list.first['duration'], list.first['explicit'], - list.first['imagePath']); + list.first['imagePath'], + list.first['media_id']); return episode; } + + Future getRssItemWithMediaId(String id) async { + var dbClient = await database; + EpisodeBrief episode; + 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.media_id = ?""", [id]); + episode = EpisodeBrief( + list.first['title'], + list.first['enclosure_url'], + list.first['enclosure_length'], + list.first['milliseconds'], + list.first['feed_title'], + list.first['primaryColor'], + list.first['liked'], + list.first['downloaded'], + list.first['duration'], + list.first['explicit'], + list.first['imagePath'], + list.first['media_id']); + return episode; + } + } diff --git a/lib/main.dart b/lib/main.dart index 3aad67a..c74b54f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -10,57 +10,61 @@ 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 'local_storage/sqflite_localpodcast.dart'; +import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; + +void callbackDispatcher() { + Workmanager.executeTask((task, inputData) async { + var dbHelper = DBHelper(); + print('Start task'); + List podcastList = await dbHelper.getPodcastLocalAll(); + await Future.forEach(podcastList, (podcastLocal) async { + await dbHelper.updatePodcastRss(podcastLocal); + print('Refresh ' + podcastLocal.title); + }); + return Future.value(true); + }); +} final SettingState themeSetting = SettingState(); - Future main() async { WidgetsFlutterBinding.ensureInitialized(); await themeSetting.initData(); + await FlutterDownloader.initialize(); runApp( MultiProvider( providers: [ ChangeNotifierProvider(create: (_) => themeSetting), - ChangeNotifierProvider(create: (_) => AudioPlayer()), + ChangeNotifierProvider(create: (_) => AudioPlayerNotifier()), ChangeNotifierProvider(create: (_) => GroupList()), ChangeNotifierProvider(create: (_) => ImportOmpl()), ], child: MyApp(), ), ); - Workmanager.initialize( - callbackDispatcher, - isInDebugMode: true, - ); - Workmanager.registerPeriodicTask("2", "update_podcasts", - frequency: Duration(minutes: 1), - initialDelay: Duration(seconds: 5), - constraints: Constraints( - networkType: NetworkType.connected, - )); - - await FlutterDownloader.initialize(); await SystemChrome.setPreferredOrientations( [DeviceOrientation.portraitUp, DeviceOrientation.portraitDown]); } -void callbackDispatcher() { - Workmanager.executeTask((task, inputData) async { - var dbHelper = DBHelper(); - List podcastList = await dbHelper.getPodcastLocalAll(); - await Future.forEach(podcastList, (podcastLocal) async { - await dbHelper.updatePodcastRss(podcastLocal); - print('Refresh ' + podcastLocal.title); - }); - return true; - }); -} - class MyApp extends StatelessWidget { + void setWorkManager() { + Workmanager.initialize( + callbackDispatcher, + isInDebugMode: true, + ); + + Workmanager.registerPeriodicTask("1", "update_podcasts", + frequency: Duration(hours: 12), + initialDelay: Duration(seconds: 5), + constraints: Constraints( + networkType: NetworkType.connected, + requiresBatteryNotLow: true, + )); + } @override Widget build(BuildContext context) { return Consumer( builder: (_, setting, __) { + if (setting.autoUpdate) setWorkManager(); return MaterialApp( themeMode: setting.theme, debugShowCheckedModeBanner: false, @@ -78,7 +82,6 @@ class MyApp extends StatelessWidget { elevation: 0, ), textTheme: TextTheme( - headline1: TextStyle(fontSize: 72.0, fontWeight: FontWeight.bold), bodyText2: TextStyle(fontSize: 15.0, fontWeight: FontWeight.normal), ), @@ -89,6 +92,7 @@ class MyApp extends StatelessWidget { ), darkTheme: ThemeData.dark().copyWith( accentColor: setting.accentSetColor, + appBarTheme: AppBarTheme(elevation: 0), ), home: MyHomePage(), ); diff --git a/lib/podcasts/custom_tabview.dart b/lib/podcasts/custom_tabview.dart new file mode 100644 index 0000000..8ba248a --- /dev/null +++ b/lib/podcasts/custom_tabview.dart @@ -0,0 +1,146 @@ +import 'package:flutter/material.dart'; + +class CustomTabView extends StatefulWidget { + final int itemCount; + final IndexedWidgetBuilder tabBuilder; + final IndexedWidgetBuilder pageBuilder; + final ValueChanged onPositionChange; + final ValueChanged onScroll; + final int initPosition; + + CustomTabView({ + @required this.itemCount, + @required this.tabBuilder, + @required this.pageBuilder, + this.onPositionChange, + this.onScroll, + this.initPosition, + }); + + @override + _CustomTabsState createState() => _CustomTabsState(); +} + +class _CustomTabsState extends State + with TickerProviderStateMixin { + TabController controller; + int _currentCount; + int _currentPosition; + + @override + void initState() { + _currentPosition = widget.initPosition ?? 0; + controller = TabController( + length: widget.itemCount, + vsync: this, + initialIndex: _currentPosition, + ); + controller.addListener(onPositionChange); + controller.animation.addListener(onScroll); + _currentCount = widget.itemCount; + super.initState(); + } + + @override + void didUpdateWidget(CustomTabView oldWidget) { + if (_currentCount != widget.itemCount) { + controller.animation.removeListener(onScroll); + controller.removeListener(onPositionChange); + controller.dispose(); + + if (widget.initPosition != null) { + _currentPosition = widget.initPosition; + } + + if (_currentPosition > widget.itemCount - 1) { + _currentPosition = widget.itemCount - 1; + _currentPosition = _currentPosition < 0 ? 0 : _currentPosition; + if (widget.onPositionChange is ValueChanged) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (mounted) { + widget.onPositionChange(_currentPosition); + } + }); + } + } + + _currentCount = widget.itemCount; + setState(() { + controller = TabController( + length: widget.itemCount, + vsync: this, + initialIndex: _currentPosition, + ); + controller.addListener(onPositionChange); + controller.animation.addListener(onScroll); + }); + } else if (widget.initPosition != null) { + controller.animateTo(widget.initPosition); + } + + super.didUpdateWidget(oldWidget); + } + + @override + void dispose() { + controller.animation.removeListener(onScroll); + controller.removeListener(onPositionChange); + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + children: [ + Container( + alignment: Alignment.centerLeft, + height: 50.0, + padding: EdgeInsets.all(10.0), + child: TabBar( + indicatorSize: TabBarIndicatorSize.label, + labelPadding: EdgeInsets.symmetric(horizontal: 5.0), + indicatorPadding: EdgeInsets.symmetric(horizontal: 5.0), + isScrollable: true, + controller: controller, + labelColor: Colors.white, + unselectedLabelColor: Colors.grey[700], + indicator: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(15)), + color: Theme.of(context).accentColor, + ), + tabs: List.generate( + widget.itemCount, + (index) => widget.tabBuilder(context, index), + ), + ), + ), + Expanded( + child: TabBarView( + controller: controller, + children: List.generate( + widget.itemCount, + (index) => widget.pageBuilder(context, index), + ), + ), + ), + ], + ); + } + + onPositionChange() { + if (!controller.indexIsChanging) { + _currentPosition = controller.index; + if (widget.onPositionChange is ValueChanged) { + widget.onPositionChange(_currentPosition); + } + } + } + + onScroll() { + if (widget.onScroll is ValueChanged) { + widget.onScroll(controller.animation.value); + } + } +} diff --git a/lib/podcasts/podcastdetail.dart b/lib/podcasts/podcastdetail.dart index 96d081b..dd86985 100644 --- a/lib/podcasts/podcastdetail.dart +++ b/lib/podcasts/podcastdetail.dart @@ -7,16 +7,13 @@ import 'package:flutter/services.dart'; import 'package:html/parser.dart'; import 'package:url_launcher/url_launcher.dart'; -import 'package:google_fonts/google_fonts.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:tsacdop/class/podcastlocal.dart'; import 'package:tsacdop/class/episodebrief.dart'; -import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/util/episodegrid.dart'; -import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/home/audioplayer.dart'; import 'package:tsacdop/class/fireside_data.dart'; import 'package:tsacdop/util/colorize.dart'; @@ -104,7 +101,9 @@ class _PodcastDetailState extends State { fit: BoxFit.cover)), alignment: Alignment.centerRight, child: Container( - color: Theme.of(context).scaffoldBackgroundColor.withOpacity(0.5), + color: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.5), padding: EdgeInsets.symmetric(vertical: 5.0), width: MediaQuery.of(context).size.width, alignment: Alignment.centerRight, @@ -122,7 +121,8 @@ class _PodcastDetailState extends State { children: [ CircleAvatar( backgroundColor: Colors.grey[400], - backgroundImage: CachedNetworkImageProvider( + backgroundImage: + CachedNetworkImageProvider( host.image, )), Padding( @@ -162,7 +162,7 @@ class _PodcastDetailState extends State { @override Widget build(BuildContext context) { - double _width = MediaQuery.of(context).size.width; + Color _color = widget.podcastLocal.primaryColor.colorizedark(); return AnnotatedRegion( value: SystemUiOverlayStyle( @@ -283,7 +283,7 @@ class _PodcastDetailState extends State { 'Hosted on ' + widget.podcastLocal .provider, - maxLines: 1, + maxLines: 1, style: TextStyle( color: Colors.white), ) @@ -308,6 +308,8 @@ class _PodcastDetailState extends State { ), title: top < 70 ? Text(widget.podcastLocal.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, style: TextStyle(color: Colors.white)) : Center(), ); @@ -322,184 +324,15 @@ class _PodcastDetailState extends State { ), ), SliverPadding( - padding: - const EdgeInsets.symmetric(horizontal: 15.0), - sliver: SliverGrid( - gridDelegate: - SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1.0, - crossAxisCount: 3, - mainAxisSpacing: 6.0, - crossAxisSpacing: 6.0, - ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - EpisodeBrief episodeBrief = - snapshot.data[index]; - Color _c = (Theme.of(context).brightness == - Brightness.light) - ? widget.podcastLocal.primaryColor - .colorizedark() - : widget.podcastLocal.primaryColor - .colorizeLight(); - - return Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - Navigator.push( - context, - ScaleRoute( - page: EpisodeDetail( - episodeItem: episodeBrief, - heroTag: 'podcast', - )), - ); - }, - child: Container( - decoration: BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(5.0)), - color: Theme.of(context) - .scaffoldBackgroundColor, - border: Border.all( - color: Theme.of(context) - .brightness == - Brightness.light - ? Theme.of(context) - .primaryColor - : Theme.of(context) - .scaffoldBackgroundColor, - width: 3.0, - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context) - .primaryColor, - blurRadius: 0.5, - spreadRadius: 0.5, - ), - ]), - alignment: Alignment.center, - padding: EdgeInsets.all(8.0), - child: Column( - mainAxisAlignment: - MainAxisAlignment.center, - children: [ - Expanded( - flex: 2, - child: Row( - mainAxisAlignment: - MainAxisAlignment.start, - children: [ - Hero( - tag: episodeBrief - .enclosureUrl + - 'podcast', - child: Container( - height: _width / 16, - width: _width / 16, - child: CircleAvatar( - backgroundImage: - FileImage(File( - "${episodeBrief.imagePath}")), - ), - ), - ), - Spacer(), - Container( - alignment: - Alignment.topRight, - child: Text( - (snapshot.data.length - - index) - .toString(), - style: GoogleFonts.teko( - textStyle: TextStyle( - fontSize: - _width / 24, - color: _c, - ), - ), - ), - ) - ], - ), - ), - Expanded( - flex: 5, - child: Container( - alignment: Alignment.topLeft, - padding: - EdgeInsets.only(top: 2.0), - child: Text( - episodeBrief.title, - style: TextStyle( - fontSize: _width / 32, - ), - maxLines: 4, - overflow: TextOverflow.fade, - ), - ), - ), - Expanded( - flex: 1, - child: Row( - children: [ - Align( - alignment: - Alignment.bottomLeft, - child: Text( - episodeBrief - .dateToString(), - //podcast[index].pubDate.substring(4, 16), - style: TextStyle( - fontSize: - _width / 35, - color: _c, - fontStyle: FontStyle - .italic), - ), - ), - Spacer(), - DownloadIcon( - episodeBrief: - episodeBrief), - Padding( - padding: - EdgeInsets.all(1), - ), - Container( - alignment: - Alignment.bottomRight, - child: (episodeBrief - .liked == - 0) - ? Center() - : IconTheme( - data: - IconThemeData( - size: 15), - child: Icon( - Icons.favorite, - color: - Colors.red, - ), - ), - ), - ], - ), - ), - ], - ), - ), - ), - ); - }, - childCount: snapshot.data.length, - ), - ), - ), + padding: const EdgeInsets.symmetric( + horizontal: 10.0), + sliver: EpisodeGrid( + podcast: snapshot.data, + showDownload: false, + showFavorite: true, + showNumber: true, + heroTag: 'podcast', + )), ], ) : Center(child: CircularProgressIndicator()); diff --git a/lib/podcasts/podcastgroup.dart b/lib/podcasts/podcastgroup.dart index e3cc80a..a888f4f 100644 --- a/lib/podcasts/podcastgroup.dart +++ b/lib/podcasts/podcastgroup.dart @@ -1,12 +1,10 @@ import 'dart:io'; -import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; -import 'package:flutter_colorpicker/flutter_colorpicker.dart'; import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/class/podcastlocal.dart'; @@ -21,87 +19,7 @@ class PodcastGroupList extends StatefulWidget { _PodcastGroupListState createState() => _PodcastGroupListState(); } -class _PodcastGroupListState extends State - with SingleTickerProviderStateMixin { - bool _showSetting; - AnimationController _controller; - Animation _animation; - double _fraction; - - @override - void initState() { - super.initState(); - _showSetting = false; - _fraction = 0; - _controller = AnimationController( - duration: const Duration(milliseconds: 500), vsync: this); - _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) - ..addListener(() { - if (mounted) - setState(() { - _fraction = _animation.value; - }); - }); - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed) { - _controller.stop(); - } else if (status == AnimationStatus.dismissed) { - _controller.stop(); - } - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - Widget _saveButton(BuildContext context) { - var podcastList = widget.group.podcasts; - var _groupList = Provider.of(context, listen: false); - return Transform( - alignment: FractionalOffset(0.5, 0.5), - transform: Matrix4.rotationY(math.pi * _fraction), - child: Container( - child: InkWell( - child: Container( - width: 50, - height: 50, - decoration: BoxDecoration( - color: _fraction > 0.5 ? Colors.red : widget.group.getColor(), - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey[700], - blurRadius: 5, - offset: Offset(1, 1), - ), - ]), - alignment: Alignment.center, - child: Icon( - _fraction > 0.5 ? Icons.save : Icons.settings, - color: Colors.white, - )), - onTap: () async { - if (_fraction == 0) { - setState(() { - _showSetting = true; - }); - } else { - await _groupList.saveOrder(widget.group, podcastList); - Fluttertoast.showToast( - msg: 'Setting Saved', - gravity: ToastGravity.BOTTOM, - ); - _controller.reverse(); - } - }, - ), - ), - ); - } - +class _PodcastGroupListState extends State { @override Widget build(BuildContext context) { var groupList = Provider.of(context, listen: false); @@ -109,372 +27,38 @@ class _PodcastGroupListState extends State ? Container( color: Theme.of(context).primaryColor, ) - : Stack( - children: [ - Container( - color: Theme.of(context).primaryColor, - child: Stack( - children: [ - ReorderableListView( - onReorder: (int oldIndex, int newIndex) { - setState(() { - if (newIndex > oldIndex) { - newIndex -= 1; - } - final PodcastLocal podcast = - widget.group.podcasts.removeAt(oldIndex); - widget.group.podcasts.insert(newIndex, podcast); - _controller.forward(); - }); - }, - children: widget.group.podcasts - .map((PodcastLocal podcastLocal) { - return Container( - decoration: BoxDecoration( - color: Theme.of(context).primaryColor), - key: ObjectKey(podcastLocal.title), - child: PodcastCard( - podcastLocal: podcastLocal, - group: widget.group, - ), - ); - }).toList(), - ), - Positioned( - bottom: 30, - right: 30, - child: _saveButton(context), - ), - ], + : Container( + color: Theme.of(context).primaryColor, + child: Stack( + children: [ + ReorderableListView( + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final PodcastLocal podcast = + widget.group.podcasts.removeAt(oldIndex); + widget.group.podcasts.insert(newIndex, podcast); + }); + widget.group.setOrderedPodcasts = widget.group.podcasts; + groupList.addToOrderChanged(widget.group.name); + }, + children: widget.group.podcasts + .map((PodcastLocal podcastLocal) { + return Container( + decoration: + BoxDecoration(color: Theme.of(context).primaryColor), + key: ObjectKey(podcastLocal.title), + child: PodcastCard( + podcastLocal: podcastLocal, + group: widget.group, + ), + ); + }).toList(), ), - ), - _showSetting - ? Positioned.fill( - child: GestureDetector( - onTap: () => setState(() => _showSetting = false), - child: Container( - color: Theme.of(context) - .scaffoldBackgroundColor - .withOpacity(0.5), - ), - ), - ) - : Center(), - _showSetting - ? Container( - alignment: Alignment.bottomCenter, - child: Container( - height: 150.0, - alignment: Alignment.center, - decoration: BoxDecoration( - color: Theme.of(context).primaryColor, - boxShadow: [ - BoxShadow( - offset: Offset(0, -1), - blurRadius: 4, - color: Theme.of(context).brightness == - Brightness.light - ? Colors.grey[400] - : Colors.grey[800], - ), - ], - ), - child: SingleChildScrollView( - scrollDirection: Axis.vertical, - child: Container( - height: 150, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - // mainAxisSize: MainAxisSize.min, - children: [ - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - setState(() => _showSetting = false); - showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: - MaterialLocalizations.of(context) - .modalBarrierDismissLabel, - barrierColor: Colors.black54, - transitionDuration: - const Duration(milliseconds: 200), - pageBuilder: (BuildContext context, - Animation animaiton, - Animation - secondaryAnimation) => - AnnotatedRegion< - SystemUiOverlayStyle>( - value: SystemUiOverlayStyle( - statusBarIconBrightness: - Brightness.light, - systemNavigationBarColor: - Theme.of(context) - .brightness == - Brightness.light - ? Color.fromRGBO( - 113, - 113, - 113, - 1) - : Color.fromRGBO( - 15, 15, 15, 1), - statusBarColor: Theme.of( - context) - .brightness == - Brightness.light - ? Color.fromRGBO( - 113, 113, 113, 1) - : Color.fromRGBO( - 5, 5, 5, 1), - ), - child: SafeArea( - child: AlertDialog( - elevation: 1, - titlePadding: - EdgeInsets.only( - top: 20, - left: 40, - right: 200, - bottom: 20), - shape: RoundedRectangleBorder( - borderRadius: - BorderRadius.all( - Radius.circular( - 10.0))), - title: - Text('Choose a color'), - content: - SingleChildScrollView( - child: MaterialPicker( - onColorChanged: - (value) { - PodcastGroup newGroup = - PodcastGroup( - widget - .group.name, - color: value - .toString() - .substring( - 10, - 16), - id: widget - .group.id, - podcastList: - widget - .group - .podcastList); - groupList.updateGroup( - newGroup); - Navigator.of(context) - .pop(); - }, - pickerColor: - Colors.blue, - ), - ), - )))); - }, - child: Container( - height: 50.0, - padding: - EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - Icon(Icons.colorize), - Padding( - padding: EdgeInsets.symmetric( - horizontal: 5.0), - ), - Text('Change Color'), - ], - ), - ), - ), - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - setState(() => _showSetting = false); - widget.group.name == 'Home' - ? 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: 200), - pageBuilder: (BuildContext - context, - Animation animaiton, - Animation - secondaryAnimation) => - RenameGroup( - group: widget.group, - )); - }, - child: Container( - height: 50.0, - padding: - EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - Icon(Icons.text_fields), - Padding( - padding: EdgeInsets.symmetric( - horizontal: 5.0), - ), - Text('Rename'), - ], - ), - ), - ), - ), - Material( - color: Colors.transparent, - child: InkWell( - onTap: () { - setState(() => _showSetting = false); - widget.group.name == 'Home' - ? 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: 200), - pageBuilder: (BuildContext - context, - Animation animaiton, - Animation - secondaryAnimation) => - AnnotatedRegion< - SystemUiOverlayStyle>( - value: SystemUiOverlayStyle( - statusBarIconBrightness: - Brightness.light, - systemNavigationBarColor: - Theme.of(context) - .brightness == - Brightness - .light - ? Color.fromRGBO( - 113, - 113, - 113, - 1) - : Color.fromRGBO( - 15, - 15, - 15, - 1), - statusBarColor: Theme.of( - context) - .brightness == - Brightness.light - ? Color.fromRGBO( - 113, 113, 113, 1) - : Color.fromRGBO( - 5, 5, 5, 1), - ), - child: SafeArea( - 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: () { - groupList.delGroup( - widget.group); - Navigator.of( - context) - .pop(); - }, - child: Text( - 'CONFIRM', - style: TextStyle( - color: Colors - .red), - ), - ) - ], - ), - ), - )); - }, - child: Container( - height: 50, - padding: - EdgeInsets.symmetric(horizontal: 20), - child: Row( - children: [ - Icon(Icons.delete_outline), - Padding( - padding: EdgeInsets.symmetric( - horizontal: 5.0), - ), - Text('Delete'), - ], - ), - ), - ), - ), - ], - ), - ), - ), - ), - ) - : Center(), - ], + ], + ), ); } } @@ -853,14 +437,14 @@ class _RenameGroupState extends State { style: TextStyle(color: Theme.of(context).accentColor)), ) ], - title: Text('Create new group'), + title: Text('Edit group name'), content: Column( mainAxisSize: MainAxisSize.min, children: [ TextField( decoration: InputDecoration( contentPadding: EdgeInsets.symmetric(horizontal: 10), - hintText: 'New Group', + hintText: widget.group.name, hintStyle: TextStyle(fontSize: 18), filled: true, focusedBorder: UnderlineInputBorder( diff --git a/lib/podcasts/podcastmanage.dart b/lib/podcasts/podcastmanage.dart index 0f90252..e9898f3 100644 --- a/lib/podcasts/podcastmanage.dart +++ b/lib/podcasts/podcastmanage.dart @@ -1,23 +1,135 @@ +import 'dart:math' as math; + +import 'package:flutter/gestures.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; +import 'package:line_icons/line_icons.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/podcasts/podcastgroup.dart'; import 'package:tsacdop/podcasts/podcastlist.dart'; import 'package:tsacdop/util/pageroute.dart'; +import 'custom_tabview.dart'; class PodcastManage extends StatefulWidget { @override _PodcastManageState createState() => _PodcastManageState(); } -class _PodcastManageState extends State { - Decoration getIndicator() { - return const UnderlineTabIndicator( - borderSide: BorderSide(color: Colors.red, width: 0), - insets: EdgeInsets.only( - top: 10.0, - )); +class _PodcastManageState extends State + with TickerProviderStateMixin { + bool _showSetting; + double _menuValue; + AnimationController _controller; + AnimationController _menuController; + Animation _animation; + Animation _menuAnimation; + double _fraction; + int _index; + double _scroll; + @override + void initState() { + super.initState(); + _showSetting = false; + _fraction = 0; + _menuValue = 0; + _scroll = 0; + _index = 0; + _menuController = AnimationController( + duration: const Duration(milliseconds: 300), vsync: this); + _controller = AnimationController( + duration: const Duration(milliseconds: 500), vsync: this); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _fraction = _animation.value; + }); + }); + _menuAnimation = Tween(begin: 0.0, end: 1.0).animate( + CurvedAnimation(parent: _menuController, curve: Curves.easeInOutBack)) + ..addListener(() { + if (mounted) setState(() => _menuValue = _menuAnimation.value); + }); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.stop(); + } else if (status == AnimationStatus.dismissed) { + _controller.stop(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + _menuController.dispose(); + super.dispose(); + } + + Widget _saveButton(BuildContext context) { + return Consumer( + builder: (_, groupList, __) { + if (groupList.orderChanged.contains(groupList.groups[_index].name)) { + _controller.forward(); + } else if (_fraction > 0) { + _controller.reverse(); + } + return Transform( + alignment: FractionalOffset(0.5, 0.5), + transform: Matrix4.rotationY(math.pi * _fraction), + child: Container( + child: InkWell( + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: _fraction > 0.5 + ? Colors.red + : Theme.of(context).accentColor, + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey[700], + blurRadius: 5, + offset: Offset(1, 1), + ), + ]), + alignment: Alignment.center, + child: _fraction > 0.5 + ? Icon(LineIcons.save_solid, color: Colors.white) + : AnimatedIcon( + color: Colors.white, + icon: AnimatedIcons.menu_close, + progress: _menuController, + ), + // color: Colors.white, + ), + onTap: () async { + if (_fraction == 0) { + !_showSetting + ? _menuController.forward() + : await _menuController.reverse(); + setState(() { + _showSetting = !_showSetting; + }); + } else { + groupList.saveOrder(groupList.groups[_index]); + groupList.drlFromOrderChanged(groupList.groups[_index].name); + Fluttertoast.showToast( + msg: 'Setting Saved', + gravity: ToastGravity.BOTTOM, + ); + _controller.reverse(); + } + }, + ), + ), + ); + }, + ); } Widget build(BuildContext context) { @@ -53,59 +165,300 @@ class _PodcastManageState extends State { List _groups = groupList.groups; return _isLoading ? Center() - : DefaultTabController( - length: _groups.length, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 50, - padding: EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.centerLeft, - child: TabBar( - labelColor: Colors.white, - unselectedLabelColor: Colors.black, - labelPadding: EdgeInsets.all(5.0), - indicator: getIndicator(), - isScrollable: true, - tabs: _groups.map((group) { - return Tab( - child: Container( - height: 30.0, - padding: - EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - decoration: BoxDecoration( - color: group.getColor(), - // Theme.of(context).brightness == - // Brightness.light - // ? Theme.of(context).primaryColorDark - // : Colors.grey[800], - borderRadius: - BorderRadius.all(Radius.circular(15)), - ), - child: Text( - group.name, - )), - ); - - }).toList(), - ), - ), - Expanded( + : Stack( + children: [ + CustomTabView( + itemCount: _groups.length, + tabBuilder: (context, index) => Tab( child: Container( - child: TabBarView( - children: _groups.map((group) { - return Container( - key: ObjectKey(group), - child: PodcastGroupList(group: group)); - }).toList(), - ), - ), - ) - ], - )); + 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( + 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: SafeArea( + 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( + '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(), + ], + ); }), ), ), diff --git a/lib/settings/downloads_manage.dart b/lib/settings/downloads_manage.dart new file mode 100644 index 0000000..4c3d608 --- /dev/null +++ b/lib/settings/downloads_manage.dart @@ -0,0 +1,287 @@ +import 'dart:io'; + +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:flutter_downloader/flutter_downloader.dart'; +import 'package:path_provider/path_provider.dart'; +import 'package:line_icons/line_icons.dart'; +import 'package:tsacdop/class/episodebrief.dart'; +import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; + +class DownloadsManage extends StatefulWidget { + @override + _DownloadsManageState createState() => _DownloadsManageState(); +} + +class _DownloadsManageState extends State { + //Downloaded size + int _size; + //Downloaded files + int _fileNum; + bool _loadEpisodes; + bool _clearing; + List _selectedList; + List _episodes = []; + + _getDownloadedRssItem() async { + _episodes = []; + final tasks = await FlutterDownloader.loadTasksWithRawQuery( + query: "SELECT * FROM task WHERE status = 3"); + var dbHelper = DBHelper(); + await Future.forEach(tasks, (task) async { + EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.url); + _episodes.add(episode); + }); + setState(() { + _loadEpisodes = true; + }); + } + + _getStorageSize() async { + _size = 0; + _fileNum = 0; + var dir = await getExternalStorageDirectory(); + dir.list().forEach((d) { + var fileDir = Directory(d.path); + fileDir.list().forEach((file) async { + await File(file.path).stat().then((value) { + _size += value.size; + _fileNum += 1; + setState(() {}); + }); + }); + }); + } + + _delSelectedEpisodes() async { + setState(() => _clearing = true); + await Future.forEach(_selectedList, (EpisodeBrief episode) async { + print(episode.downloaded); + await FlutterDownloader.remove( + taskId: episode.downloaded, shouldDeleteContent: true); + var dbHelper = DBHelper(); + await dbHelper.delDownloaded(episode.enclosureUrl); + setState(() => + _episodes.removeWhere((e) => e.enclosureUrl == episode.enclosureUrl)); + }); + await Future.delayed(Duration(seconds: 1)); + setState(() { + _clearing = false; + }); + await Future.delayed(Duration(seconds: 1)); + setState(() => _selectedList = []); + _getStorageSize(); + } + + int sumSelected() { + int sum = 0; + if (_selectedList.length == 0) { + return sum; + } else { + _selectedList.forEach((episode) { + sum += episode.enclosureLength; + }); + return sum; + } + } + + @override + void initState() { + super.initState(); + _clearing = false; + _loadEpisodes = false; + _selectedList = []; + _getStorageSize(); + _getDownloadedRssItem(); + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: Theme.of(context).accentColorBrightness, + systemNavigationBarColor: Theme.of(context).primaryColor, + statusBarColor: Theme.of(context).primaryColor), + child: SafeArea( + child: Scaffold( + appBar: AppBar( + title: Text('Downloads'), + elevation: 0, + backgroundColor: Theme.of(context).primaryColor, + ), + body: Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Padding( + padding: EdgeInsets.all(10.0), + ), + Container( + height: 100.0, + padding: EdgeInsets.only(bottom: 40, left: 60), + alignment: Alignment.centerLeft, + child: RichText( + text: TextSpan( + text: 'Total ', + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 20, + ), + children: [ + TextSpan( + text: _fileNum.toString(), + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 40, + fontWeight: FontWeight.bold)), + TextSpan( + text: ' episodes ', + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 20, + )), + TextSpan( + text: (_size ~/ 1000000).toString(), + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 60, + fontWeight: FontWeight.bold)), + TextSpan( + text: ' Mb', + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 20, + )), + ], + ), + ), + ), + _loadEpisodes + ? Expanded( + child: ListView.builder( + itemCount: _episodes.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + itemBuilder: (context, index) { + return Column( + children: [ + ListTile( + onTap: () { + if (_selectedList + .contains(_episodes[index])) { + setState(() => _selectedList + .removeWhere((episode) => + episode.enclosureUrl == + _episodes[index] + .enclosureUrl)); + } else { + setState(() => _selectedList + .add(_episodes[index])); + } + }, + leading: CircleAvatar( + backgroundImage: FileImage(File( + "${_episodes[index].imagePath}")), + ), + title: Text( + _episodes[index].title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), + subtitle: _episodes[index] + .enclosureLength != + 0 + ? Text(((_episodes[index] + .enclosureLength) ~/ + 1000000) + .toString() + + ' Mb') + : Center(), + trailing: Checkbox( + value: _selectedList + .contains(_episodes[index]), + onChanged: (bool boo) { + print(boo); + if (boo) { + setState(() => _selectedList + .add(_episodes[index])); + } else { + setState(() => _selectedList + .removeWhere((episode) => + episode.enclosureUrl == + _episodes[index] + .enclosureUrl)); + } + }, + ), + ), + Divider( + height: 2, + ), + ], + ); + }), + ) + : CircularProgressIndicator(), + ], + ), + AnimatedPositioned( + duration: Duration(milliseconds: 800), + curve: Curves.elasticInOut, + left: MediaQuery.of(context).size.width / 2 - 50, + bottom: _selectedList.length == 0 ? -100 : 30, + child: InkWell( + onTap: () => _delSelectedEpisodes(), + child: Stack( + alignment: _clearing + ? Alignment.centerLeft + : Alignment.centerRight, + children: [ + Container( + alignment: Alignment.center, + width: 100, + height: 40, + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(20.0)), + color: Theme.of(context).accentColor, + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Icon( + LineIcons.trash_alt_solid, + color: Colors.white, + ), + Text((sumSelected() ~/ 1000000).toString() + 'Mb', + style: TextStyle(color: Colors.white)), + ], + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: AnimatedContainer( + duration: Duration(milliseconds: 500), + alignment: Alignment.center, + width: _clearing ? 100 : 0, + height: _clearing ? 40 : 0, + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(20.0)), + color: Colors.red.withOpacity(0.6), + ), + ), + ), + ], + )), + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/settings/history.dart b/lib/settings/history.dart new file mode 100644 index 0000000..c6c83e4 --- /dev/null +++ b/lib/settings/history.dart @@ -0,0 +1,390 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:fl_chart/fl_chart.dart'; +import 'package:intl/intl.dart'; +import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; +import 'package:tsacdop/class/audiostate.dart'; +import 'package:tsacdop/class/sub_history.dart'; + +class PlayedHistory extends StatefulWidget { + @override + _PlayedHistoryState createState() => _PlayedHistoryState(); +} + +class _PlayedHistoryState extends State + with SingleTickerProviderStateMixin { + Future> getPlayHistory() async { + DBHelper dbHelper = DBHelper(); + List playHistory; + playHistory = await dbHelper.getPlayHistory(); + await Future.forEach(playHistory, (playHistory) async { + await playHistory.getEpisode(); + }); + return playHistory; + } + + Future> getSubHistory() async { + DBHelper dbHelper = DBHelper(); + return await dbHelper.getSubHistory(); + } + + static String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } + + TabController _controller; + List list = const [0, 1, 2, 3, 4, 5, 6]; + + Future> getData() async { + var dbHelper = DBHelper(); + List stats = []; + await Future.forEach(list, (day) async { + double mins = await dbHelper.listenMins(7 - day); + stats.add(FlSpot(day.toDouble(), mins)); + }); + return stats; + } + + @override + void initState() { + super.initState(); + _controller = TabController(length: 2, vsync: this); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + double top = 0; + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: Theme.of(context).accentColorBrightness, + systemNavigationBarColor: Theme.of(context).primaryColor, + statusBarColor: Theme.of(context).primaryColor, + ), + child: SafeArea( + child: Scaffold( + body: NestedScrollView( + headerSliverBuilder: + (BuildContext context, bool innerBoxScrolled) { + return [ + SliverAppBar( + elevation: 0, + expandedHeight: 260, + floating: false, + pinned: true, + flexibleSpace: LayoutBuilder( + builder: + (BuildContext context, BoxConstraints constraints) { + top = constraints.biggest.height; + return FlexibleSpaceBar( + title: top < 70 + ? Text( + 'History', + ) + : Center(), + background: Padding( + padding: EdgeInsets.only( + top: 50, left: 50, right: 50, bottom: 30), + child: FutureBuilder>( + future: getData(), + builder: (context, snapshot) { + return snapshot.hasData + ? HistoryChart(snapshot.data) + : Center(); + }), + ), + ); + }, + ), + ), + SliverPersistentHeader( + delegate: _SliverAppBarDelegate( + TabBar( + controller: _controller, + tabs: [ + Tab( + child: Text('Listen'), + ), + Tab( + child: Text('Subscribe'), + ) + ], + ), + Theme.of(context).primaryColor), + pinned: true, + ), + ]; + }, + body: TabBarView(controller: _controller, children: [ + FutureBuilder>( + future: getPlayHistory(), + builder: (context, snapshot) { + double _width = MediaQuery.of(context).size.width; + return snapshot.hasData + ? ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.vertical, + itemCount: snapshot.data.length, + itemBuilder: (BuildContext context, int index) { + return Column( + children: [ + ListTile( + title: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + DateFormat.yMd().add_jm().format( + snapshot.data[index].playdate), + style: TextStyle( + color: const Color(0xff67727d), + fontSize: 15, + fontStyle: FontStyle.italic), + ), + Text( + snapshot.data[index].title, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + ], + ), + subtitle: Container( + width: _width, + child: Row( + children: [ + Icon(Icons.timelapse, color: Colors.grey[400],), + Container( + height: 2, + decoration: BoxDecoration( + border: Border(bottom: BorderSide(color: Colors.grey[400], width: 2.0)) + ), + width: _width * + snapshot.data[index] + .seekValue < + (_width - 120) + ? _width * + snapshot + .data[index].seekValue + : _width - 120, + ), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 2), + ), + Container( + width: 50, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context) + .accentColor, + borderRadius: BorderRadius.all( + Radius.circular(10))), + padding: EdgeInsets.all(2), + child: Text( + _stringForSeconds( + snapshot.data[index].seconds), + style: TextStyle( + color: Colors.white), + ), + ), + ], + ), + ), + ), + // Divider(height: 2), + ], + ); + }) + : Center( + child: CircularProgressIndicator(), + ); + }, + ), + FutureBuilder>( + future: getSubHistory(), + builder: (context, snapshot) { + return snapshot.hasData + ? ListView.builder( + shrinkWrap: true, + scrollDirection: Axis.vertical, + itemCount: snapshot.data.length, + itemBuilder: (BuildContext context, int index) { + bool _status = snapshot.data[index].status; + return Column( + children: [ + ListTile( + enabled: _status, + title: Column( + mainAxisAlignment: + MainAxisAlignment.start, + crossAxisAlignment: + CrossAxisAlignment.start, + children: [ + Text( + DateFormat.yMd().add_jm().format( + snapshot.data[index].subDate), + style: TextStyle( + color: const Color(0xff67727d), + fontSize: 15, + fontStyle: FontStyle.italic), + ), + Text(snapshot.data[index].title), + ], + ), + subtitle: Row( + children: [ + _status + ? Text(DateTime.now() + .difference(snapshot + .data[index].subDate) + .inDays + .toString() + + ' days') + : Text(snapshot.data[index].delDate + .difference(snapshot + .data[index].subDate) + .inDays + .toString() + + ' days'), + Spacer(), + !_status + ? Text( + 'Removed at ' + + DateFormat.yMd() + .add_jm() + .format(snapshot + .data[index] + .delDate), + style: TextStyle( + color: Colors.red), + ) + : Center(), + ], + ), + ), + Divider( + height: 2, + ) + ], + ); + }) + : Center( + child: CircularProgressIndicator(), + ); + }, + ), + ])), + ), + ), + ); + } +} + +class _SliverAppBarDelegate extends SliverPersistentHeaderDelegate { + _SliverAppBarDelegate(this._tabBar, this._color); + final Color _color; + final TabBar _tabBar; + + @override + double get minExtent => _tabBar.preferredSize.height; + @override + double get maxExtent => _tabBar.preferredSize.height; + + @override + Widget build( + BuildContext context, double shrinkOffset, bool overlapsContent) { + return new Container( + color: _color, + child: _tabBar, + ); + } + + @override + bool shouldRebuild(_SliverAppBarDelegate oldDelegate) { + return true; + } +} + +class HistoryChart extends StatelessWidget { + final List stats; + HistoryChart(this.stats); + @override + Widget build(BuildContext context) { + return LineChart( + LineChartData( + backgroundColor: Colors.transparent, + gridData: FlGridData( + show: true, + drawHorizontalLine: true, + getDrawingHorizontalLine: (value) { + return value % 60 == 0 + ? FlLine( + color: Theme.of(context).brightness == Brightness.light + ? Colors.grey[400] + : Colors.grey[700], + strokeWidth: 1, + ) + : FlLine(color: Colors.transparent); + }, + ), + titlesData: FlTitlesData( + show: true, + bottomTitles: SideTitles( + textStyle: TextStyle( + color: const Color(0xff67727d), + fontWeight: FontWeight.bold, + fontSize: 12, + ), + showTitles: true, + reservedSize: 10, + getTitles: (value) { + return DateFormat.E().format( + DateTime.now().subtract(Duration(days: (7 - value.toInt())))); + }, + margin: 5, + ), + leftTitles: SideTitles( + showTitles: true, + textStyle: TextStyle( + color: const Color(0xff67727d), + fontWeight: FontWeight.bold, + fontSize: 12, + ), + getTitles: (value) { + return value % 60 == 0 && value > 0 ? '${value ~/ 60}h' : ''; + }, + reservedSize: 20, + margin: 5, + ), + ), + borderData: FlBorderData( + show: false, + border: Border( + left: BorderSide(color: Colors.red, width: 2), + )), + lineBarsData: [ + LineChartBarData( + spots: this.stats, + isCurved: false, + colors: [Theme.of(context).accentColor], + barWidth: 3, + isStrokeCapRound: true, + dotData: FlDotData( + show: true, + dotSize: 5, + dotColor: Theme.of(context).accentColor, + ), + ), + ], + ), + ); + } +} diff --git a/lib/settings/libries.dart b/lib/settings/libries.dart new file mode 100644 index 0000000..3657a35 --- /dev/null +++ b/lib/settings/libries.dart @@ -0,0 +1,90 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:url_launcher/url_launcher.dart'; +import 'licenses.dart'; + +class Libries extends StatelessWidget { + _launchUrl(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } + } + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: Theme.of(context).accentColorBrightness, + systemNavigationBarColor: Theme.of(context).primaryColor, + statusBarColor: Theme.of(context).primaryColor), + child: SafeArea( + child: Scaffold( + appBar: AppBar( + title: Text('Libraies'), + elevation: 0, + backgroundColor: Theme.of(context).primaryColor, + ), + body: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: 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('Google', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: Theme.of(context).accentColor)), + ), + Column( + children: google.map( + (e) { + return ListTile( + contentPadding: EdgeInsets.symmetric(horizontal: 80), + onTap: () => _launchUrl(e.link), + title: Text(e.name), + subtitle: Text(e.license), + ); + }, + ).toList(), + ), + Container( + height: 30.0, + padding: EdgeInsets.symmetric(horizontal: 80), + alignment: Alignment.centerLeft, + child: Text('Plugins', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: Theme.of(context).accentColor)), + ), + Container( + child: Column( + children: plugins.map( + (e) { + return ListTile( + onTap: () => _launchUrl(e.link), + contentPadding: EdgeInsets.symmetric(horizontal: 80), + title: Text(e.name), + subtitle: Text(e.license), + ); + }, + ).toList(), + ), + ), + ], + ), + ), + ), + ), + ); + } +} diff --git a/lib/settings/licenses.dart b/lib/settings/licenses.dart new file mode 100644 index 0000000..b28a95d --- /dev/null +++ b/lib/settings/licenses.dart @@ -0,0 +1,44 @@ +const String apacheLicense = "Apache License 2.0"; +const String mit = "MIT License"; +const String bsd ="BSD 3-Clause"; +const String gpl = "GPL 3.0"; + +class Libries { + String name; String license; String link; + Libries(this.name, this.license, this.link); +} +List google = [ + Libries('Android X', apacheLicense, 'https://source.android.com/setup/start/licenses'), + Libries('Flutter', bsd, 'https://github.com/flutter/flutter/blob/master/LICENSE') +]; + +List plugins = [ + Libries('json_annotation',bsd, 'https://pub.dev/packages/json_annotation'), + Libries('sqflite', mit, 'https://pub.dev/packages/sqflite'), + Libries('flutter_html', mit, 'https://pub.dev/packages/flutter_html'), + Libries('path_provider', bsd, 'https://pub.dev/packages/path_provider'), + Libries('color_thief_flutter', mit, 'https://pub.dev/packages/color_thief_flutter'), + Libries('provider', mit, 'https://pub.dev/packages/provider'), + Libries('google_fonts', apacheLicense, 'https://pub.dev/packages/google_fonts'), + Libries('dio', mit, 'https://pub.dev/packages/dio'), + Libries('file_picker', mit, 'https://pub.dev/packages/file_picker'), + Libries('xml', mit, 'https://pub.dev/packages/xml'), + Libries('marquee', mit, 'https://pub.dev/packages/marquee'), + Libries('flutter_downloader', bsd, 'https://pub.dev/packages/flutter_downloader'), + Libries('permission_handler', mit, 'https://pub.dev/packages/permission_handler'), + Libries('fluttertoast', mit, 'https://pub.dev/packages/fluttertoast'), + Libries('intl', bsd, 'https://pub.dev/packages/intl'), + Libries('url_launcher', bsd, 'https://pub.dev/packages/url_launcher'), + Libries('image', apacheLicense, 'https://pub.dev/packages/image'), + Libries('shared_preferences', bsd, 'https://pub.dev/packages/shared_preferences'), + Libries('uuid', mit, 'https://pub.dev/packages/uuid'), + Libries('tuple', bsd, 'https://pub.dev/packages/tuple'), + Libries('cached_network_image', mit, 'https://pub.dev/packages/cached_network_image'), + Libries('workmanager', mit, 'https://pub.dev/packages/workmanager'), + Libries('flutter_colorpicker', mit, 'https://pub.dev/packages/flutter_colorpicker'), + Libries('app_settings', mit, 'https://pub.dev/packages/app_settings'), + Libries('fl_chart', bsd, 'https://pub.dev/packages/fl_chart'), + Libries('audio_service', mit, 'https://pub.dev/packages/audio_service'), + Libries('just_audio', apacheLicense, 'https://pub.dev/packages/just_audio'), + Libries('line_icons', gpl, 'https://pub.dev/packages/line_icons'), +]; \ No newline at end of file diff --git a/lib/settings/settting.dart b/lib/settings/settting.dart index 7071ca3..6c1d63e 100644 --- a/lib/settings/settting.dart +++ b/lib/settings/settting.dart @@ -1,10 +1,30 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; +import 'package:provider/provider.dart'; +import 'package:workmanager/workmanager.dart'; +import 'package:line_icons/line_icons.dart'; +import 'package:url_launcher/url_launcher.dart'; + +import 'package:tsacdop/class/audiostate.dart'; +import 'package:tsacdop/class/settingstate.dart'; import 'package:tsacdop/settings/theme.dart'; +import 'package:tsacdop/settings/storage.dart'; +import 'package:tsacdop/settings/history.dart'; +import 'libries.dart'; class Settings extends StatelessWidget { + _launchUrl(String url) async { + if (await canLaunch(url)) { + await launch(url); + } else { + throw 'Could not launch $url'; + } + } + @override Widget build(BuildContext context) { + var audio = Provider.of(context, listen: false); + var settings = Provider.of(context, listen: false); return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Theme.of(context).accentColorBrightness, @@ -18,121 +38,167 @@ class Settings extends StatelessWidget { elevation: 0, backgroundColor: Theme.of(context).primaryColor, ), - body: Column( - 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('Prefrence', - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: Theme.of(context).accentColor)), - ), - ListView( - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - ListTile( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => ThemeSetting())), - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.colorize), - title: Text('Appearance'), - subtitle: Text('Colors and themes'), - ), - Divider(height: 2), - ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.network_check), - title: Text('Network'), - subtitle: Text('Download network setting'), - ), - Divider(height: 2), - ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.storage), - title: Text('Cache'), - subtitle: Text('Manage and clear cache'), - ), - Divider(height: 2), - ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.update), - title: Text('Update'), - subtitle: Text('Update in background'), - ), - Divider(height: 2), - ], - ), - ], - ), - Padding( - padding: EdgeInsets.all(10.0), - ), - Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 30.0, - padding: EdgeInsets.symmetric(horizontal: 80), - alignment: Alignment.centerLeft, - child: Text('Info', - style: Theme.of(context) - .textTheme - .bodyText1 - .copyWith(color: Theme.of(context).accentColor)), - ), - ListView( - shrinkWrap: true, - scrollDirection: Axis.vertical, - children: [ - ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.colorize), - title: Text('Changelog'), - subtitle: Text('List of chagnes'), - ), - Divider(height: 2), - ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.network_check), - title: Text('Credit'), - subtitle: Text('Open source libraried in application'), - ), - Divider(height: 2), - ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.storage), - title: Text('Cache'), - subtitle: Text('Manage and clear cache'), - ), - Divider(height: 2), - ListTile( - contentPadding: EdgeInsets.symmetric(horizontal: 25.0), - leading: Icon(Icons.update), - title: Text('Update'), - subtitle: Text('Update in background'), - ), - Divider(height: 2), - ], - ), - ], - ), - ], + body: SingleChildScrollView( + physics: const AlwaysScrollableScrollPhysics(), + scrollDirection: Axis.vertical, + child: Column( + 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('Prefrence', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: Theme.of(context).accentColor)), + ), + ListView( + physics: ClampingScrollPhysics(), + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => ThemeSetting())), + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(LineIcons.adjust_solid), + title: Text('Appearance'), + subtitle: Text('Colors and themes'), + ), + Divider(height: 2), + ListTile( + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(LineIcons.play_circle), + title: Text('AutoPlay'), + subtitle: Text('Autoplay next episode in playlist'), + trailing: Selector( + selector: (_, audio) => audio.autoPlay, + builder: (_, data, __) => Switch( + value: data, + onChanged: (boo) => audio.autoPlaySwitch = boo), + ), + ), + Divider(height: 2), + ListTile( + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(LineIcons.cloud_download_alt_solid), + title: Text('AutoUpdate'), + subtitle: Text('Auto update feed every day'), + trailing: Selector( + selector: (_, settings) => settings.autoUpdate, + builder: (_, data, __) => Switch( + value: data, + onChanged: (boo) async { + settings.autoUpdate = boo; + if (!boo) await Workmanager.cancelAll(); + }), + ), + ), + Divider(height: 2), + ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => StorageSetting())), + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(LineIcons.save), + title: Text('Storage'), + subtitle: Text('Manage cache and download storage'), + ), + Divider(height: 2), + ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PlayedHistory())), + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(Icons.update), + title: Text('History'), + subtitle: Text('Listen data'), + ), + Divider(height: 2), + ], + ), + ], + ), + Padding( + padding: EdgeInsets.all(10.0), + ), + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 30.0, + padding: EdgeInsets.symmetric(horizontal: 80), + alignment: Alignment.centerLeft, + child: Text('Info', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: Theme.of(context).accentColor)), + ), + ListView( + physics: ClampingScrollPhysics(), + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + ListTile( + onTap: () => _launchUrl( + 'https://github.com/stonega/tsacdop/releases'), + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(LineIcons.map_signs_solid), + title: Text('Changelog'), + subtitle: Text('List of chagnes'), + ), + Divider(height: 2), + ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => Libries())), + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(LineIcons.book_open_solid), + title: Text('Libraries'), + subtitle: + Text('Open source libraried in application'), + ), + Divider(height: 2), + ListTile( + onTap: () => _launchUrl( + 'mailto:?subject=Tsacdop Feedback'), + contentPadding: + EdgeInsets.symmetric(horizontal: 25.0), + leading: Icon(LineIcons.bug_solid), + title: Text('Feedback'), + subtitle: Text('Bugs and feature requests'), + ), + Divider(height: 2), + ], + ), + ], + ), + ], + ), ), ), ), diff --git a/lib/settings/storage.dart b/lib/settings/storage.dart new file mode 100644 index 0000000..1d029b5 --- /dev/null +++ b/lib/settings/storage.dart @@ -0,0 +1,74 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:app_settings/app_settings.dart'; +import 'package:tsacdop/settings/downloads_manage.dart'; + +class StorageSetting extends StatelessWidget { + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: Theme.of(context).accentColorBrightness, + systemNavigationBarColor: Theme.of(context).primaryColor, + statusBarColor: Theme.of(context).primaryColor), + child: SafeArea( + child: Scaffold( + appBar: AppBar( + title: Text('Storage'), + elevation: 0, + backgroundColor: Theme.of(context).primaryColor, + ), + body: Column( + 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('Storage', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: Theme.of(context).accentColor)), + ), + ListView( + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + ListTile( + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => DownloadsManage())), + contentPadding: EdgeInsets.symmetric(horizontal: 80.0), + title: Text('Downloads'), + subtitle: Text('Manage doanloaded audio files'), + ), + Divider(height: 2), + ListTile( + onTap: () => AppSettings.openAppSettings(), + contentPadding: EdgeInsets.symmetric(horizontal: 80.0), + // leading: Icon(Icons.colorize), + title: Text('Cache'), + subtitle: Text('Audio cache'), + ), + Divider(height: 2), + ], + ), + ], + ), + ], + ), + ), + ), + ); + } +} diff --git a/lib/settings/theme.dart b/lib/settings/theme.dart index b5add8f..e2bd12a 100644 --- a/lib/settings/theme.dart +++ b/lib/settings/theme.dart @@ -7,7 +7,7 @@ import 'package:tsacdop/class/settingstate.dart'; class ThemeSetting extends StatelessWidget { @override Widget build(BuildContext context) { - var settings = Provider.of(context); + var settings = Provider.of(context, listen: false); return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Theme.of(context).accentColorBrightness, diff --git a/lib/util/colorize.dart b/lib/util/colorize.dart index 26a80fd..60ae0cc 100644 --- a/lib/util/colorize.dart +++ b/lib/util/colorize.dart @@ -22,7 +22,8 @@ extension Colorize on String { _c = Color.fromRGBO((255 - color[0]), 255 - color[1], 255 - color[2], 1.0); } else { - _c = Color.fromRGBO(color[0], color[1], color[2], 1.0); + _c = Color.fromRGBO(color[0] < 50 ? 100 : color[0], + color[1] < 50 ? 100 : color[1], color[2] < 50 ? 100 : color[2], 1.0); } return _c; } diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index ddac6d9..5bb3dc8 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -5,6 +5,11 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:google_fonts/google_fonts.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; +import 'package:line_icons/line_icons.dart'; +import 'package:fluttertoast/fluttertoast.dart'; +import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tsacdop/util/pageroute.dart'; @@ -24,32 +29,119 @@ class EpisodeGrid extends StatelessWidget { this.showNumber, this.heroTag}) : super(key: key); - + Offset offset; @override Widget build(BuildContext context) { double _width = MediaQuery.of(context).size.width; - return CustomScrollView( - physics: const AlwaysScrollableScrollPhysics(), - primary: false, - slivers: [ - SliverPadding( - padding: const EdgeInsets.all(5.0), - sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1.0, - crossAxisCount: 3, - mainAxisSpacing: 6.0, - crossAxisSpacing: 6.0, + + _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, + bool isPlaying, bool isInPlaylist) async { + var audio = Provider.of(context, listen: false); + double left = offset.dx; + double top = offset.dy; + await showMenu( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10))), + context: context, + position: RelativeRect.fromLTRB(left, top, _width - left, 0), + + items: >[ + PopupMenuItem( + value: 0, + child: Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.max, + children: [ + Icon( + LineIcons.play_circle_solid, + color: Theme.of(context).accentColor, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + !isPlaying ? Text('Play') : Text('Playing'), + ], ), - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - Color _c = - (Theme.of(context).brightness == Brightness.light) - ? podcast[index].primaryColor.colorizedark() - : podcast[index].primaryColor.colorizeLight(); - return Material( + ), + PopupMenuItem( + value: 1, + child: Row( + children: [ + Icon( + LineIcons.clock_solid, + color: Colors.red, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + !isInPlaylist ? Text('Later') : Text('Remove') + ], + )), + ], + elevation: 5.0, + ).then((value) { + if (value == 0) { + if (!isPlaying) audio.episodeLoad(episode); + } else if (value == 1) { + if (isInPlaylist) { + audio.addToPlaylist(episode); + Fluttertoast.showToast( + msg: 'Added to playlist', + gravity: ToastGravity.BOTTOM, + ); + } else { + audio.delFromPlaylist(episode); + Fluttertoast.showToast( + msg: 'Removed from playlist', + gravity: ToastGravity.BOTTOM, + ); + } + } + }); + } + + return SliverPadding( + padding: const EdgeInsets.all(5.0), + sliver: SliverGrid( + gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + childAspectRatio: 1.0, + crossAxisCount: 3, + mainAxisSpacing: 6.0, + crossAxisSpacing: 6.0, + ), + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + Color _c = (Theme.of(context).brightness == Brightness.light) + ? podcast[index].primaryColor.colorizedark() + : podcast[index].primaryColor.colorizeLight(); + return Selector>>( + selector: (_, audio) => Tuple2(audio?.episode, + audio.queue.playlist.map((e) => e.enclosureUrl).toList()), + builder: (_, data, __) => Container( + decoration: BoxDecoration( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + color: Theme.of(context).scaffoldBackgroundColor, + boxShadow: [ + BoxShadow( + color: Theme.of(context).primaryColor, + blurRadius: 0.5, + spreadRadius: 0.5, + ), + ]), + alignment: Alignment.center, + child: Material( color: Colors.transparent, child: InkWell( + borderRadius: BorderRadius.all(Radius.circular(5.0)), + onTapDown: (details) => offset = Offset( + details.globalPosition.dx, details.globalPosition.dy), + onLongPress: () => _showPopupMenu( + offset, + podcast[index], + context, + data.item1 == podcast[index], + data.item2.contains(podcast[index].enclosureUrl)), onTap: () { Navigator.push( context, @@ -61,25 +153,17 @@ class EpisodeGrid extends StatelessWidget { ); }, child: Container( + padding: const EdgeInsets.all(8.0), decoration: BoxDecoration( - borderRadius: BorderRadius.all(Radius.circular(5.0)), - color: Theme.of(context).scaffoldBackgroundColor, - border: Border.all( - color: - Theme.of(context).brightness == Brightness.light - ? Theme.of(context).primaryColor - : Theme.of(context).scaffoldBackgroundColor, - width: 3.0, - ), - boxShadow: [ - BoxShadow( - color: Theme.of(context).primaryColor, - blurRadius: 0.5, - spreadRadius: 0.5, - ), - ]), - alignment: Alignment.center, - padding: EdgeInsets.all(8.0), + borderRadius: BorderRadius.all(Radius.circular(5.0)), + border: Border.all( + color: + Theme.of(context).brightness == Brightness.light + ? Theme.of(context).primaryColor + : Theme.of(context).scaffoldBackgroundColor, + width: 1.0, + ), + ), child: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ @@ -94,6 +178,7 @@ class EpisodeGrid extends StatelessWidget { height: _width / 16, width: _width / 16, child: CircleAvatar( + backgroundColor: _c.withOpacity(0.5), backgroundImage: FileImage( File("${podcast[index].imagePath}")), ), @@ -140,7 +225,6 @@ class EpisodeGrid extends StatelessWidget { alignment: Alignment.bottomLeft, child: Text( podcast[index].dateToString(), - //podcast[index].pubDate.substring(4, 16), style: TextStyle( fontSize: _width / 35, color: _c, @@ -175,13 +259,13 @@ class EpisodeGrid extends StatelessWidget { ), ), ), - ); - }, - childCount: podcast.length, - ), - ), + ), + ), + ); + }, + childCount: podcast.length, ), - ], + ), ); } } diff --git a/preview/Screenshot_data.png b/preview/Screenshot_data.png new file mode 100644 index 0000000..c718ec3 Binary files /dev/null and b/preview/Screenshot_data.png differ diff --git a/preview/Screenshot_episode.png b/preview/Screenshot_episode.png new file mode 100644 index 0000000..fdeb4f0 Binary files /dev/null and b/preview/Screenshot_episode.png differ diff --git a/preview/Screenshot_homepage .png b/preview/Screenshot_homepage .png new file mode 100644 index 0000000..cc107a8 Binary files /dev/null and b/preview/Screenshot_homepage .png differ diff --git a/preview/Screenshot_podcast.png b/preview/Screenshot_podcast.png new file mode 100644 index 0000000..c366cb1 Binary files /dev/null and b/preview/Screenshot_podcast.png differ diff --git a/pubspec.yaml b/pubspec.yaml index 73282e7..0232840 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: An easy-use podacasts player. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.1.1 +version: 0.1.2 environment: sdk: ">=2.6.0 <3.0.0" @@ -28,7 +28,7 @@ dev_dependencies: flutter_test: sdk: flutter json_annotation: ^3.0.1 - sqflite: ^1.2.1 + sqflite: ^1.2.2+1 flutter_html: ^0.11.1 path_provider: ^1.6.1 color_thief_flutter: ^1.0.1 @@ -38,7 +38,6 @@ dev_dependencies: file_picker: ^1.4.3+2 xml: ^3.5.0 marquee: ^1.3.1 - audiofileplayer: ^1.1.1 flutter_downloader: ^1.4.1 permission_handler: ^4.3.0 fluttertoast: ^3.1.3 @@ -49,11 +48,16 @@ dev_dependencies: uuid: ^2.0.4 tuple: ^1.0.3 cached_network_image: ^2.0.0 - workmanager: ^0.2.0 - font_awesome_flutter: ^8.7.0 + workmanager: ^0.2.2 flutter_colorpicker: ^0.3.2 - lazy_loading_list: ^1.0.0+1 - + app_settings: ^3.0.1 + fl_chart: ^0.8.3 + audio_service: ^0.6.2 + just_audio: ^0.1.3 + rxdart: ^0.23.1 + line_icons: + git: + url: https://github.com/galonsos/line_icons.git # For information on the generic Dart part of this file, see the