From f1e49a28332b4ce4e3f4e31217dff85a42c35118 Mon Sep 17 00:00:00 2001 From: stonegate Date: Sun, 12 Apr 2020 01:23:12 +0800 Subject: [PATCH] Improve sleep timer --- .circleci/config.yml | 2 +- lib/class/audiostate.dart | 261 ++++++---- lib/class/download_state.dart | 9 +- lib/class/settingstate.dart | 2 +- lib/episodes/episodedetail.dart | 519 +++++--------------- lib/home/audioplayer.dart | 435 ++++++++++++---- lib/home/download_list.dart | 130 ++--- lib/home/home_groups.dart | 35 +- lib/home/nested_home.dart | 301 +++++++++++- lib/home/playlist.dart | 40 +- lib/local_storage/sqflite_localpodcast.dart | 224 +++++++-- lib/podcasts/podcastdetail.dart | 168 +++++-- lib/podcasts/podcastlist.dart | 6 - lib/podcasts/podcastmanage.dart | 52 +- lib/settings/history.dart | 2 +- lib/settings/licenses.dart | 1 + lib/util/custompaint.dart | 476 ++++++++++++++++++ lib/util/episodegrid.dart | 454 +++++++++++------ pubspec.yaml | 1 + 19 files changed, 2146 insertions(+), 972 deletions(-) create mode 100644 lib/util/custompaint.dart diff --git a/.circleci/config.yml b/.circleci/config.yml index 4ec4b10..c6c609d 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,7 +2,7 @@ version: 2 jobs: build: docker: - - image: cirrusci/flutter:v1.15.17 + - image: cirrusci/flutter:beta branches: only: master diff --git a/lib/class/audiostate.dart b/lib/class/audiostate.dart index aadf2d8..535614c 100644 --- a/lib/class/audiostate.dart +++ b/lib/class/audiostate.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:audio_service/audio_service.dart'; import 'package:just_audio/just_audio.dart'; +import 'package:rxdart/rxdart.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/local_storage/key_value_storage.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; @@ -59,12 +60,11 @@ class PlayHistory { } } -class Playlist extends ChangeNotifier { +class Playlist { String name; DBHelper dbHelper = DBHelper(); List _playlist; - //list of miediaitem List get playlist => _playlist; KeyValueStorage storage = KeyValueStorage('playlist'); @@ -80,7 +80,6 @@ class Playlist extends ChangeNotifier { if (episode != null) _playlist.add(episode); }); } - print('Playlist: ' + _playlist.length.toString()); } savePlaylist() async { @@ -108,6 +107,8 @@ class Playlist extends ChangeNotifier { } } +enum SleepTimerMode { endOfEpisode, timer, undefined } + class AudioPlayerNotifier extends ChangeNotifier { DBHelper dbHelper = DBHelper(); KeyValueStorage storage = KeyValueStorage('audioposition'); @@ -128,6 +129,7 @@ class AudioPlayerNotifier extends ChangeNotifier { int _timeLeft = 0; bool _startSleepTimer = false; double _switchValue = 0; + SleepTimerMode _sleepTimerMode = SleepTimerMode.undefined; bool _autoPlay = true; DateTime _current; int _currentPosition; @@ -145,6 +147,7 @@ class AudioPlayerNotifier extends ChangeNotifier { EpisodeBrief get episode => _episode; bool get stopOnComplete => _stopOnComplete; bool get startSleepTimer => _startSleepTimer; + SleepTimerMode get sleepTimerMode => _sleepTimerMode; bool get autoPlay => _autoPlay; int get timeLeft => _timeLeft; double get switchValue => _switchValue; @@ -159,6 +162,11 @@ class AudioPlayerNotifier extends ChangeNotifier { notifyListeners(); } + set setSleepTimerMode(SleepTimerMode timer) { + _sleepTimerMode = timer; + notifyListeners(); + } + @override void addListener(VoidCallback listener) async { super.addListener(listener); @@ -173,7 +181,7 @@ class AudioPlayerNotifier extends ChangeNotifier { _lastPostion = await storage.getInt(); if (_lastPostion > 0 && _queue.playlist.length > 0) { final EpisodeBrief episode = _queue.playlist.first; - final int duration = episode.enclosureLength * 60; + final int duration = episode.duration * 1000; final double seekValue = duration != 0 ? _lastPostion / duration : 1; final PlayHistory history = PlayHistory( episode.title, episode.enclosureUrl, _lastPostion / 1000, seekValue); @@ -196,22 +204,25 @@ class AudioPlayerNotifier extends ChangeNotifier { await _queue.savePlaylist(); } else { await _queue.getPlaylist(); - _queue.playlist - .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl); - _queue.playlist.insert(0, episodeNew); - _queue.savePlaylist(); + // _queue.playlist + // .removeWhere((item) => item.enclosureUrl == episode.enclosureUrl); + await _queue.delFromPlaylist(episode); + await _queue.addToPlayListAt(episodeNew, 0); _backgroundAudioDuration = 0; _backgroundAudioPosition = 0; _seekSliderValue = 0; _episode = episodeNew; _playerRunning = true; + _audioState = BasicPlaybackState.connecting; notifyListeners(); - await _queue.savePlaylist(); + //await _queue.savePlaylist(); _startAudioService(0); } } _startAudioService(int position) async { + _stopOnComplete = false; + _sleepTimerMode = SleepTimerMode.undefined; if (!AudioService.connected) { await AudioService.connect(); } @@ -223,7 +234,7 @@ class AudioPlayerNotifier extends ChangeNotifier { enableQueue: true, androidStopOnRemoveTask: true, androidStopForegroundOnPause: true); - _playerRunning = true; + if (_autoPlay) { await Future.forEach(_queue.playlist, (episode) async { await AudioService.addQueueItem(episode.toMediaItem()); @@ -231,33 +242,64 @@ class AudioPlayerNotifier extends ChangeNotifier { } else { await AudioService.addQueueItem(_queue.playlist.first.toMediaItem()); } + _playerRunning = true; await AudioService.play(); - AudioService.currentMediaItemStream.listen((item) async { - if (item != null) { - _episode = await dbHelper.getRssItemWithMediaId(item.id); - _backgroundAudioDuration = item?.duration ?? 0; - if (position > 0 && _backgroundAudioDuration > 0) { - AudioService.seekTo(position); - position = 0; - } + + AudioService.currentMediaItemStream + .where((event) => event != null) + .listen((item) async { + _episode = await dbHelper.getRssItemWithMediaId(item.id); + _backgroundAudioDuration = item?.duration ?? 0; + if (position > 0 && _backgroundAudioDuration > 0) { + AudioService.seekTo(position); + position = 0; } notifyListeners(); }); + var queueSubject = BehaviorSubject>(); + queueSubject.addStream( + AudioService.queueStream.distinct().where((event) => event != null)); + queueSubject.stream.listen((event) { + print(event.length); + if (event.length == _queue.playlist.length - 1 && + _audioState == BasicPlaybackState.skippingToNext) { + if (event.length == 0 || _stopOnComplete == true) { + _queue.delFromPlaylist(_episode); + _lastPostion = 0; + storage.saveInt(_lastPostion); + final PlayHistory history = PlayHistory( + _episode.title, + _episode.enclosureUrl, + backgroundAudioPosition / 1000, + seekSliderValue); + dbHelper.saveHistory(history); + } else if (event.first.id != _episode.mediaId) { + _queue.delFromPlaylist(_episode); + final PlayHistory history = PlayHistory( + _episode.title, + _episode.enclosureUrl, + backgroundAudioPosition / 1000, + seekSliderValue); + dbHelper.saveHistory(history); + } + } + }); + AudioService.playbackStateStream.listen((event) async { _current = DateTime.now(); _audioState = event?.basicState; - if (_audioState == BasicPlaybackState.skippingToNext && - _episode != null) { - print(_episode.title); - _queue.delFromPlaylist(_episode); - } - if (_audioState == BasicPlaybackState.skippingToNext && - _episode != null && - _backgroundAudioPosition > 0) { - PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, - _backgroundAudioPosition / 1000, _seekSliderValue); - await dbHelper.saveHistory(history); - } + // if (_audioState == BasicPlaybackState.skippingToNext && + // _episode != null) { + // print(_episode.title); + // _queue.delFromPlaylist(_episode); + // } + // if (_audioState == BasicPlaybackState.skippingToNext && + // _episode != null && + // _backgroundAudioPosition > 0) { + // PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, + // _backgroundAudioPosition / 1000, _seekSliderValue); + // await dbHelper.saveHistory(history); + // } if (_audioState == BasicPlaybackState.stopped) _playerRunning = false; if (_audioState == BasicPlaybackState.error) { @@ -296,23 +338,23 @@ class AudioPlayerNotifier extends ChangeNotifier { storage.saveInt(_lastPostion); } - if ((_queue.playlist.length == 1 || !_autoPlay) && - _seekSliderValue > 0.9 && - _episode != null) { - _queue.delFromPlaylist(_episode); - _lastPostion = 0; - storage.saveInt(_lastPostion); - final PlayHistory history = PlayHistory( - _episode.title, - _episode.enclosureUrl, - backgroundAudioPosition / 1000, - seekSliderValue); - dbHelper.saveHistory(history); - } + // if ((_queue.playlist.length == 1 || !_autoPlay) && + // _seekSli;lderValue > 0.9 && + // _episode != null && + // _audioState != BasicPlaybackState.connecting) { + // _queue.delFromPlaylist(_episode); + // _lastPostion = 0; + // storage.saveInt(_lastPostion); + // final PlayHistory history = PlayHistory( + // _episode.title, + // _episode.enclosureUrl, + // backgroundAudioPosition / 1000, + // seekSliderValue); + // dbHelper.saveHistory(history); + // } notifyListeners(); } - if (_audioState == BasicPlaybackState.stopped || - _playerRunning == false) { + if (_audioState == BasicPlaybackState.stopped) { timer.cancel(); } }); @@ -325,6 +367,7 @@ class AudioPlayerNotifier extends ChangeNotifier { _seekSliderValue = 0; _episode = _queue.playlist.first; _playerRunning = true; + _audioState = BasicPlaybackState.connecting; _queueUpdate = !_queueUpdate; notifyListeners(); _startAudioService(_lastPostion ?? 0); @@ -338,7 +381,6 @@ class AudioPlayerNotifier extends ChangeNotifier { if (_playerRunning) { await AudioService.addQueueItem(episode.toMediaItem()); } - print('add to playlist when not rnnning'); await _queue.addToPlayList(episode); notifyListeners(); } @@ -347,7 +389,6 @@ class AudioPlayerNotifier extends ChangeNotifier { if (_playerRunning) { await AudioService.addQueueItemAt(episode.toMediaItem(), index); } - print('add to playlist when not rnnning'); await _queue.addToPlayListAt(episode, index); _queueUpdate = !_queueUpdate; notifyListeners(); @@ -421,43 +462,60 @@ class AudioPlayerNotifier extends ChangeNotifier { //Set sleep timer sleepTimer(int mins) { - _startSleepTimer = true; - _switchValue = 1; - notifyListeners(); - _timeLeft = mins * 60; - Timer.periodic(Duration(seconds: 1), (timer) { - if (_timeLeft == 0) { - timer.cancel(); - notifyListeners(); - } else { - _timeLeft = _timeLeft - 1; - notifyListeners(); - } - }); - _stopTimer = Timer(Duration(minutes: mins), () { - _stopOnComplete = false; - _startSleepTimer = false; - _switchValue = 0; - _playerRunning = false; + if (_sleepTimerMode == SleepTimerMode.timer) { + _startSleepTimer = true; + _switchValue = 1; notifyListeners(); - AudioService.stop(); - AudioService.disconnect(); - }); + _timeLeft = mins * 60; + Timer.periodic(Duration(seconds: 1), (timer) { + if (_timeLeft == 0) { + timer.cancel(); + notifyListeners(); + } else { + _timeLeft = _timeLeft - 1; + notifyListeners(); + } + }); + _stopTimer = Timer(Duration(minutes: mins), () { + _stopOnComplete = false; + _startSleepTimer = false; + _switchValue = 0; + _playerRunning = false; + notifyListeners(); + AudioService.stop(); + AudioService.disconnect(); + }); + } else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) { + _stopOnComplete = true; + _switchValue = 1; + notifyListeners(); + if (_queue.playlist.length > 1 && _autoPlay) { + AudioService.customAction('stopAtEnd'); + } + } } //Cancel sleep timer cancelTimer() { - _stopTimer.cancel(); - _timeLeft = 0; - _startSleepTimer = false; - _switchValue = 0; - notifyListeners(); + if (_sleepTimerMode == SleepTimerMode.timer) { + _stopTimer.cancel(); + _timeLeft = 0; + _startSleepTimer = false; + _switchValue = 0; + notifyListeners(); + } else if (_sleepTimerMode == SleepTimerMode.endOfEpisode) { + AudioService.customAction('cancelStopAtEnd'); + _switchValue = 0; + _stopOnComplete = false; + notifyListeners(); + } } @override void dispose() async { await AudioService.stop(); await AudioService.disconnect(); + //_playerRunning = false; super.dispose(); } } @@ -468,6 +526,7 @@ class AudioPlayerTask extends BackgroundAudioTask { Completer _completer = Completer(); BasicPlaybackState _skipState; bool _playing; + bool _stopAtEnd; bool get hasNext => _queue.length > 0; @@ -494,20 +553,19 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onStart() async { - print('start background task'); + _stopAtEnd = false; var playerStateSubscription = _audioPlayer.playbackStateStream .where((state) => state == AudioPlaybackState.completed) .listen((state) { _handlePlaybackCompleted(); }); var eventSubscription = _audioPlayer.playbackEventStream.listen((event) { - print('buffer position' + event.bufferedPosition.toString()); if (event.playbackError != null) { _setState(state: BasicPlaybackState.error); } BasicPlaybackState state; if (event.buffering) { - state = BasicPlaybackState.buffering; + state = _skipState ?? BasicPlaybackState.buffering; } else { state = _stateToBasicState(event.state); } @@ -523,14 +581,13 @@ class AudioPlayerTask extends BackgroundAudioTask { eventSubscription.cancel(); } - void _handlePlaybackCompleted() { + void _handlePlaybackCompleted() async { if (hasNext) { onSkipToNext(); } else { - _skipState = BasicPlaybackState.skippingToNext; _audioPlayer.stop(); _queue.removeAt(0); - _skipState = null; + await AudioServiceBackground.setQueue(_queue); onStop(); } } @@ -544,32 +601,28 @@ class AudioPlayerTask extends BackgroundAudioTask { @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); - } - if (_queue.length == 0) { + _skipState = BasicPlaybackState.skippingToNext; + await _audioPlayer.stop(); + _queue.removeAt(0); + await AudioServiceBackground.setQueue(_queue); + // } + if (_queue.length == 0 || _stopAtEnd) { + _skipState = null; onStop(); } else { - AudioServiceBackground.setQueue(_queue); + // AudioServiceBackground.setQueue(_queue); AudioServiceBackground.setMediaItem(mediaItem); - _skipState = BasicPlaybackState.skippingToNext; await _audioPlayer.setUrl(mediaItem.id); - print(mediaItem.id); Duration duration = await _audioPlayer.durationFuture ?? Duration.zero; AudioServiceBackground.setMediaItem( mediaItem.copyWith(duration: duration.inMilliseconds)); _skipState = null; // Resume playback if we were playing - if (_playing) { - onPlay(); - } else { - _setState(state: BasicPlaybackState.paused); - } + // if (_playing) { + onPlay(); + // } else { + // _setState(state: BasicPlaybackState.paused); + // } } } @@ -578,7 +631,7 @@ class AudioPlayerTask extends BackgroundAudioTask { if (_skipState == null) { if (_playing == null) { _playing = true; - AudioServiceBackground.setQueue(_queue); + // await AudioServiceBackground.setQueue(_queue); await _audioPlayer.setUrl(mediaItem.id); Duration duration = await _audioPlayer.durationFuture; AudioServiceBackground.setMediaItem( @@ -618,7 +671,7 @@ class AudioPlayerTask extends BackgroundAudioTask { @override void onAddQueueItem(MediaItem mediaItem) async { _queue.add(mediaItem); - AudioServiceBackground.setQueue(_queue); + await AudioServiceBackground.setQueue(_queue); } @override @@ -633,8 +686,8 @@ class AudioPlayerTask extends BackgroundAudioTask { await _audioPlayer.stop(); _queue.removeWhere((item) => item.id == mediaItem.id); _queue.insert(0, mediaItem); - AudioServiceBackground.setQueue(_queue); - AudioServiceBackground.setMediaItem(mediaItem); + await AudioServiceBackground.setQueue(_queue); + await AudioServiceBackground.setMediaItem(mediaItem); await _audioPlayer.setUrl(mediaItem.id); Duration duration = await _audioPlayer.durationFuture ?? Duration.zero; AudioServiceBackground.setMediaItem( @@ -642,7 +695,7 @@ class AudioPlayerTask extends BackgroundAudioTask { onPlay(); } else { _queue.insert(index, mediaItem); - AudioServiceBackground.setQueue(_queue); + await AudioServiceBackground.setQueue(_queue); } } @@ -684,9 +737,11 @@ class AudioPlayerTask extends BackgroundAudioTask { @override void onCustomAction(funtion, argument) { switch (funtion) { - case 'addQueue': + case 'stopAtEnd': + _stopAtEnd = true; break; - case 'updateMedia': + case 'cancelStopAtEnd': + _stopAtEnd = false; break; } } diff --git a/lib/class/download_state.dart b/lib/class/download_state.dart index 80f7c48..ec60c6f 100644 --- a/lib/class/download_state.dart +++ b/lib/class/download_state.dart @@ -90,13 +90,16 @@ class DownloadState extends ChangeNotifier { Future _saveMediaId(EpisodeTask episodeTask) async { episodeTask.status = DownloadTaskStatus.complete; final completeTask = await FlutterDownloader.loadTasksWithRawQuery( - query: - "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'"); + query: "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'"); String filePath = 'file://' + path.join(completeTask.first.savedDir, completeTask.first.filename); - print(filePath); dbHelper.saveMediaId( episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId); + EpisodeBrief episode = + await dbHelper.getRssItemWithUrl(episodeTask.episode.enclosureUrl); + _removeTask(episodeTask.episode); + _episodeTasks.add(EpisodeTask(episode, episodeTask.taskId, + progress: 100, status: DownloadTaskStatus.complete)); } void _unbindBackgroundIsolate() { diff --git a/lib/class/settingstate.dart b/lib/class/settingstate.dart index 0dbacda..cf72ee3 100644 --- a/lib/class/settingstate.dart +++ b/lib/class/settingstate.dart @@ -125,7 +125,7 @@ class SettingState extends ChangeNotifier { int color = int.parse('FF' + colorString.toUpperCase(), radix: 16); _accentSetColor = Color(color).withOpacity(1.0); } else { - _accentSetColor = Color.fromRGBO(35, 204, 198, 1); + _accentSetColor = Colors.teal[500]; } } diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index 8073823..70165ee 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:math' as math; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -17,6 +16,7 @@ import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/util/context_extension.dart'; +import 'package:tsacdop/util/custompaint.dart'; import 'episodedownload.dart'; class EpisodeDetail extends StatefulWidget { @@ -163,7 +163,7 @@ class _EpisodeDetailState extends State { horizontal: 10.0), alignment: Alignment.center, child: Text( - (widget.episodeItem.duration) + (widget.episodeItem.duration ~/ 60) .toString() + 'mins', style: textstyle), @@ -253,10 +253,15 @@ class _EpisodeDetailState extends State { 'assets/shownote.png'), height: 100.0, ), - Padding(padding: EdgeInsets.all(5.0)), + Padding( + padding: EdgeInsets.all(5.0)), Text( - 'Still no shownote received\n for this episode.', textAlign: TextAlign.center, - style: TextStyle(color: context.textTheme.bodyText1.color.withOpacity(0.5))), + 'Still no shownote received\n for this episode.', + textAlign: TextAlign.center, + style: TextStyle( + color: context.textTheme + .bodyText1.color + .withOpacity(0.5))), ], ), ) @@ -313,7 +318,11 @@ class MenuBar extends StatefulWidget { class _MenuBarState extends State { bool _liked; - int _like; + + Future getPosition(EpisodeBrief episode) async { + var dbHelper = DBHelper(); + return await dbHelper.getPosition(episode); + } Future saveLiked(String url) async { var dbHelper = DBHelper(); @@ -328,16 +337,25 @@ class _MenuBarState extends State { if (result == 1 && mounted) setState(() { _liked = false; - _like = 0; + // _like = 0; }); return result; } + Future _isLiked(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + return await dbHelper.isLiked(episode.enclosureUrl); + } + + static String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } + @override void initState() { super.initState(); _liked = false; - _like = widget.episodeItem.liked; } Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material( @@ -384,32 +402,39 @@ class _MenuBarState extends State { ), ), ), - (_like == 0 && !_liked) - ? _buttonOnMenu( - Icon( - Icons.favorite_border, - color: Colors.grey[700], - ), - () => saveLiked(widget.episodeItem.enclosureUrl)) - : (_like == 1 && !_liked) + FutureBuilder( + future: _isLiked(widget.episodeItem), + initialData: false, + builder: (BuildContext context, AsyncSnapshot snapshot) { + return (!snapshot.data && !_liked) ? _buttonOnMenu( Icon( - Icons.favorite, - color: Colors.red, + Icons.favorite_border, + color: Colors.grey[700], ), - () => setUnliked(widget.episodeItem.enclosureUrl)) - : Stack( - alignment: Alignment.center, - children: [ - LoveOpen(), - _buttonOnMenu( - Icon( - Icons.favorite, - color: Colors.red, - ), - () => setUnliked(widget.episodeItem.enclosureUrl)), - ], - ), + () => saveLiked(widget.episodeItem.enclosureUrl)) + : (snapshot.data && !_liked) + ? _buttonOnMenu( + Icon( + Icons.favorite, + color: Colors.red, + ), + () => setUnliked(widget.episodeItem.enclosureUrl)) + : Stack( + alignment: Alignment.center, + children: [ + LoveOpen(), + _buttonOnMenu( + Icon( + Icons.favorite, + color: Colors.red, + ), + () => setUnliked( + widget.episodeItem.enclosureUrl)), + ], + ); + }, + ), DownloadButton(episode: widget.episodeItem), Selector>( selector: (_, audio) => @@ -418,8 +443,13 @@ class _MenuBarState extends State { return data.contains(widget.episodeItem.enclosureUrl) ? _buttonOnMenu( Icon(Icons.playlist_add_check, - color: Theme.of(context).accentColor), - () {}) + color: Theme.of(context).accentColor), () { + audio.delFromPlaylist(widget.episodeItem); + Fluttertoast.showToast( + msg: 'Removed from playlist', + gravity: ToastGravity.BOTTOM, + ); + }) : _buttonOnMenu( Icon(Icons.playlist_add, color: Colors.grey[700]), () { Fluttertoast.showToast( @@ -430,8 +460,61 @@ class _MenuBarState extends State { }); }, ), + FutureBuilder( + future: getPosition(widget.episodeItem), + builder: (context, snapshot) { + if (snapshot.hasError) print(snapshot.error); + return snapshot.hasData + ? snapshot.data.seekValue > 0.95 + ? Container( + height: 20, + padding: EdgeInsets.symmetric(horizontal:15), + child: SizedBox( + width: 20, + height: 20, + child: CustomPaint( + painter: ListenedPainter(context.accentColor, + stroke: 2.0), + ), + ), + ) + : snapshot.data.seconds < 0.1 + ? Center() + : Container( + height: 50, + padding: EdgeInsets.symmetric(horizontal:15), + child: Row( + children: [ + SizedBox( + width: 20, + height: 20, + child: CustomPaint( + painter: ListenedPainter( + context.accentColor, + stroke: 2.0), + ), + ), + Padding(padding: EdgeInsets.symmetric(horizontal:2)), + Container( + height: 20, + padding: + EdgeInsets.symmetric(horizontal: 2), + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(10.0)), + color: context.accentColor, + ), + child: Text( + _stringForSeconds(snapshot.data.seconds), + style: TextStyle(color: Colors.white), + ), + ), + ], + ), + ) + : Center(); + }), Spacer(), - // Text(audio.audioState.toString()), Selector>( selector: (_, audio) => Tuple2(audio.episode, audio.audioState), @@ -464,20 +547,12 @@ class _MenuBarState extends State { ), ), ) - : (widget.episodeItem.title == data.item1?.title && - data.item2 == BasicPlaybackState.playing) - ? Container( - padding: EdgeInsets.only(right: 30), - child: SizedBox( - width: 20, height: 15, child: WaveLoader())) - : Container( - padding: EdgeInsets.only(right: 30), - child: SizedBox( - width: 20, - height: 15, - child: LineLoader(), - ), - ); + : Container( + padding: EdgeInsets.only(right: 30), + child: SizedBox( + width: 20, + height: 15, + child: WaveLoader(color: context.accentColor))); }, ), ], @@ -485,351 +560,3 @@ class _MenuBarState extends State { ); } } - -class LinePainter extends CustomPainter { - double _fraction; - Paint _paint; - Color _maincolor; - LinePainter(this._fraction, this._maincolor) { - _paint = Paint() - ..color = _maincolor - ..strokeWidth = 2.0 - ..strokeCap = StrokeCap.round; - } - - @override - void paint(Canvas canvas, Size size) { - canvas.drawLine(Offset(0, size.height / 2.0), - Offset(size.width * _fraction, size.height / 2.0), _paint); - } - - @override - bool shouldRepaint(LinePainter oldDelegate) { - return oldDelegate._fraction != _fraction; - } -} - -class LineLoader extends StatefulWidget { - @override - _LineLoaderState createState() => _LineLoaderState(); -} - -class _LineLoaderState extends State - with SingleTickerProviderStateMixin { - double _fraction = 0.0; - Animation animation; - AnimationController controller; - @override - void initState() { - super.initState(); - controller = - AnimationController(vsync: this, duration: Duration(milliseconds: 500)); - animation = Tween(begin: 0.0, end: 1.0).animate(controller) - ..addListener(() { - if (mounted) - setState(() { - _fraction = animation.value; - }); - }); - controller.forward(); - controller.addStatusListener((status) { - if (status == AnimationStatus.completed) { - controller.reset(); - } else if (status == AnimationStatus.dismissed) { - controller.forward(); - } - }); - } - - @override - void dispose() { - controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: LinePainter(_fraction, Theme.of(context).accentColor)); - } -} - -class WavePainter extends CustomPainter { - double _fraction; - double _value; - Color _color; - WavePainter(this._fraction, this._color); - @override - void paint(Canvas canvas, Size size) { - if (_fraction < 0.5) { - _value = _fraction; - } else { - _value = 1 - _fraction; - } - Path _path = Path(); - Paint _paint = Paint() - ..color = _color - ..strokeWidth = 2.0 - ..strokeCap = StrokeCap.round - ..style = PaintingStyle.stroke; - _path.moveTo(0, size.height / 2); - _path.lineTo(0, size.height / 2 + size.height * _value * 0.2); - _path.moveTo(0, size.height / 2); - _path.lineTo(0, size.height / 2 - size.height * _value * 0.2); - _path.moveTo(size.width / 4, size.height / 2); - _path.lineTo(size.width / 4, size.height / 2 + size.height * _value * 0.8); - _path.moveTo(size.width / 4, size.height / 2); - _path.lineTo(size.width / 4, size.height / 2 - size.height * _value * 0.8); - _path.moveTo(size.width / 2, size.height / 2); - _path.lineTo(size.width / 2, size.height / 2 + size.height * _value * 0.5); - _path.moveTo(size.width / 2, size.height / 2); - _path.lineTo(size.width / 2, size.height / 2 - size.height * _value * 0.5); - _path.moveTo(size.width * 3 / 4, size.height / 2); - _path.lineTo( - size.width * 3 / 4, size.height / 2 + size.height * _value * 0.6); - _path.moveTo(size.width * 3 / 4, size.height / 2); - _path.lineTo( - size.width * 3 / 4, size.height / 2 - size.height * _value * 0.6); - _path.moveTo(size.width, size.height / 2); - _path.lineTo(size.width, size.height / 2 + size.height * _value * 0.2); - _path.moveTo(size.width, size.height / 2); - _path.lineTo(size.width, size.height / 2 - size.height * _value * 0.2); - canvas.drawPath(_path, _paint); - } - - @override - bool shouldRepaint(WavePainter oldDelegate) { - return oldDelegate._fraction != _fraction; - } -} - -class WaveLoader extends StatefulWidget { - @override - _WaveLoaderState createState() => _WaveLoaderState(); -} - -class _WaveLoaderState extends State - with SingleTickerProviderStateMixin { - double _fraction = 0.0; - Animation animation; - AnimationController _controller; - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, duration: Duration(milliseconds: 1000)); - animation = Tween(begin: 0.0, end: 1.0).animate(_controller) - ..addListener(() { - if (mounted) - setState(() { - _fraction = animation.value; - }); - }); - _controller.forward(); - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed) { - _controller.reset(); - } else if (status == AnimationStatus.dismissed) { - _controller.forward(); - } - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: WavePainter(_fraction, Theme.of(context).accentColor)); - } -} - -class ImageRotate extends StatefulWidget { - final String title; - final String path; - ImageRotate({this.title, this.path, Key key}) : super(key: key); - @override - _ImageRotateState createState() => _ImageRotateState(); -} - -class _ImageRotateState extends State - with SingleTickerProviderStateMixin { - Animation _animation; - AnimationController _controller; - double _value; - @override - void initState() { - super.initState(); - _value = 0; - _controller = AnimationController( - vsync: this, - duration: Duration(milliseconds: 2000), - ); - _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) - ..addListener(() { - if (mounted) - setState(() { - _value = _animation.value; - }); - }); - _controller.forward(); - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed) { - _controller.reset(); - } else if (status == AnimationStatus.dismissed) { - _controller.forward(); - } - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - @override - Widget build(BuildContext context) { - return Transform.rotate( - angle: 2 * math.pi * _value, - child: Container( - padding: EdgeInsets.all(10.0), - child: ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(15.0)), - child: Container( - height: 30.0, - width: 30.0, - color: Colors.white, - child: Image.file(File("${widget.path}")), - ), - ), - ), - ); - } -} - -class LovePainter extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - Path _path = Path(); - Paint _paint = Paint() - ..color = Colors.red - ..strokeWidth = 2.0 - ..strokeCap = StrokeCap.round; - - _path.moveTo(size.width / 2, size.height / 6); - _path.quadraticBezierTo(size.width / 4, 0, size.width / 8, size.height / 6); - _path.quadraticBezierTo( - 0, size.height / 3, size.width / 8, size.height * 0.55); - _path.quadraticBezierTo( - size.width / 4, size.height * 0.8, size.width / 2, size.height); - _path.quadraticBezierTo(size.width * 0.75, size.height * 0.8, - size.width * 7 / 8, size.height * 0.55); - _path.quadraticBezierTo( - size.width, size.height / 3, size.width * 7 / 8, size.height / 6); - _path.quadraticBezierTo( - size.width * 3 / 4, 0, size.width / 2, size.height / 6); - - canvas.drawPath(_path, _paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} - -class LoveOpen extends StatefulWidget { - @override - _LoveOpenState createState() => _LoveOpenState(); -} - -class _LoveOpenState extends State - with SingleTickerProviderStateMixin { - Animation _animationA; - AnimationController _controller; - - @override - void initState() { - super.initState(); - _controller = AnimationController( - vsync: this, - duration: Duration(milliseconds: 300), - ); - - _animationA = Tween(begin: 0.0, end: 1.0).animate(_controller) - ..addListener(() { - if (mounted) setState(() {}); - }); - - _controller.forward(); - _controller.addStatusListener((status) { - if (status == AnimationStatus.completed) { - _controller.reset(); - } - }); - } - - @override - void dispose() { - _controller.dispose(); - super.dispose(); - } - - Widget _littleHeart(double scale, double value, double angle) => Container( - alignment: Alignment.centerLeft, - padding: EdgeInsets.only(left: value), - child: ScaleTransition( - scale: _animationA, - alignment: Alignment.center, - child: Transform.rotate( - angle: angle, - child: SizedBox( - height: 5 * scale, - width: 6 * scale, - child: CustomPaint( - painter: LovePainter(), - ), - ), - ), - ), - ); - - @override - Widget build(BuildContext context) { - return Container( - width: 50, - height: 50, - alignment: Alignment.center, - child: Column( - mainAxisAlignment: MainAxisAlignment.spaceEvenly, - children: [ - Row( - children: [ - _littleHeart(0.5, 10, -math.pi / 6), - _littleHeart(1.2, 3, 0), - ], - ), - Row( - children: [ - _littleHeart(0.8, 6, math.pi * 1.5), - _littleHeart(0.9, 24, math.pi / 2), - ], - ), - Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - _littleHeart(1, 8, -math.pi * 0.7), - _littleHeart(0.8, 8, math.pi), - _littleHeart(0.6, 3, -math.pi * 1.2) - ], - ), - ], - ), - ); - } -} diff --git a/lib/home/audioplayer.dart b/lib/home/audioplayer.dart index 51c5992..6d19cd4 100644 --- a/lib/home/audioplayer.dart +++ b/lib/home/audioplayer.dart @@ -1,5 +1,4 @@ import 'dart:io'; -import 'dart:ui' as ui; import 'dart:math' as math; import 'package:flutter/material.dart'; @@ -17,7 +16,9 @@ import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tsacdop/home/audiopanel.dart'; import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/util/colorize.dart'; +import 'package:tsacdop/util/context_extension.dart'; import 'package:tsacdop/util/day_night_switch.dart'; +import 'package:tsacdop/util/custompaint.dart'; class MyRectangularTrackShape extends RectangularSliderTrackShape { Rect getPreferredRect({ @@ -85,18 +86,6 @@ class MyRoundSliderThumpShape extends SliderComponentShape { ..strokeWidth = 2, ); - // Path _path = Path(); - - // _path.moveTo(center.dx - 12, center.dy + 10); - // _path.lineTo(center.dx - 12, center.dy - 12); - // _path.lineTo(center.dx -12, center.dy - 12); - // canvas.drawShadow(_path, Colors.black, 4, false); - - // Path _pathLight = Path(); - // _pathLight.moveTo(center.dx + 12, center.dy - 12); - // _pathLight.lineTo(center.dx + 12, center.dy + 10); - //// _pathLight.lineTo(center.dx - 12, center.dy + 10); - // canvas.drawShadow(_pathLight, Colors.black, 4, true); canvas.drawRect( Rect.fromLTRB( center.dx - 10, center.dy + 10, center.dx + 10, center.dy - 10), @@ -117,33 +106,33 @@ class MyRoundSliderThumpShape extends SliderComponentShape { } } +final List _customShadow = [ + BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white), + BoxShadow( + blurRadius: 8, + offset: Offset(2, 2), + color: Colors.grey[600].withOpacity(0.4)) +]; + +final List _customShadowNight = [ + BoxShadow( + blurRadius: 6, + offset: Offset(-1, -1), + color: Colors.grey[100].withOpacity(0.3)), + BoxShadow(blurRadius: 8, offset: Offset(2, 2), color: Colors.black) +]; + +String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; +} + class PlayerWidget extends StatefulWidget { @override _PlayerWidgetState createState() => _PlayerWidgetState(); } class _PlayerWidgetState extends State { - static String _stringForSeconds(double seconds) { - if (seconds == null) return null; - return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; - } - - List _customShadow = [ - BoxShadow(blurRadius: 26, offset: Offset(-6, -6), color: Colors.white), - BoxShadow( - blurRadius: 8, - offset: Offset(2, 2), - color: Colors.grey[600].withOpacity(0.4)) - ]; - - List _customShadowNight = [ - BoxShadow( - blurRadius: 6, - offset: Offset(-1, -1), - color: Colors.grey[100].withOpacity(0.3)), - BoxShadow(blurRadius: 8, offset: Offset(2, 2), color: Colors.black) - ]; - List minsToSelect = [10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99]; int _minSelected; final GlobalKey miniPlaylistKey = GlobalKey(); @@ -243,7 +232,9 @@ class _PlayerWidgetState extends State { moonColor: Colors.grey[600], dayColor: Theme.of(context).primaryColorDark, nightColor: Colors.black, - onDrag: (value) => audio.setSwitchValue = value, + onDrag: (value) { + audio.setSwitchValue = value; + }, onChanged: (value) { if (value) { audio.sleepTimer(_minSelected); @@ -380,7 +371,10 @@ class _PlayerWidgetState extends State { BasicPlaybackState .buffering || data.audioState == - BasicPlaybackState.connecting + BasicPlaybackState + .connecting || + data.audioState == + BasicPlaybackState.none ? 'Buffring...' : '', style: TextStyle( @@ -618,7 +612,7 @@ class _PlayerWidgetState extends State { // color: context.primaryColorDark, alignment: Alignment.centerLeft, child: Text( - 'Playlist', + 'Queue', style: TextStyle( color: Theme.of(context).accentColor, fontWeight: FontWeight.bold, @@ -654,8 +648,6 @@ class _PlayerWidgetState extends State { audio.playNext(); miniPlaylistKey.currentState.removeItem( 0, (context, animation) => Container()); - miniPlaylistKey.currentState.removeItem( - 1, (context, animation) => Container()); miniPlaylistKey.currentState.insertItem(0); }, child: SizedBox( @@ -689,8 +681,7 @@ class _PlayerWidgetState extends State { Navigator.push( context, SlideLeftRoute(page: PlaylistPage()), - )..then((value) => - miniPlaylistKey.currentState.initState()); + ); }, child: SizedBox( height: 30.0, @@ -723,7 +714,7 @@ class _PlayerWidgetState extends State { itemBuilder: (context, index, animation) => ScaleTransition( alignment: Alignment.center, scale: animation, - child: index == 0 || index > data.item1.length -1 + child: index == 0 || index > data.item1.length - 1 ? Center() : Column( children: [ @@ -858,7 +849,7 @@ class _PlayerWidgetState extends State { children: [ TabBarView( children: [ - _sleppMode(context), + SleepMode(), _controlPanel(context), _playlist(context), ], @@ -1120,10 +1111,11 @@ class _LastPositionState extends State { builder: (context, snapshot) { if (snapshot.hasError) print(snapshot.error); return snapshot.hasData - ? snapshot.data.seekValue > 0.95 + ? snapshot.data.seekValue > 0.90 ? Container( height: 20.0, alignment: Alignment.center, + padding: EdgeInsets.symmetric(horizontal: 5), decoration: BoxDecoration( border: Border.all( width: 1, @@ -1232,63 +1224,6 @@ class _ImageRotateState extends State } } -class StarSky extends CustomPainter { - @override - void paint(Canvas canvas, Size size) { - final points = [ - Offset(50, 100), - Offset(150, 75), - Offset(250, 250), - Offset(130, 200), - Offset(270, 150), - ]; - final pisces = [ - Offset(9, 4), - Offset(11, 5), - Offset(7, 6), - Offset(10, 7), - Offset(8, 8), - Offset(9, 13), - Offset(12, 17), - Offset(5, 19), - Offset(7, 19) - ].map((e) => e * 10).toList(); - final orion = [ - Offset(3, 1), - Offset(6, 1), - Offset(1, 4), - Offset(2, 4), - Offset(2, 7), - Offset(10, 8), - Offset(3, 10), - Offset(8, 10), - Offset(19, 11), - Offset(11, 13), - Offset(18, 14), - Offset(5, 19), - Offset(7, 19), - Offset(9, 18), - Offset(15, 19), - Offset(16, 18), - Offset(2, 25), - Offset(10, 26) - ].map((e) => Offset(e.dx * 10 + 250, e.dy * 10)).toList(); - - Paint paint = Paint() - ..color = Colors.white - ..strokeWidth = 2.0 - ..strokeCap = StrokeCap.round; - canvas.drawPoints(ui.PointMode.points, pisces, paint); - canvas.drawPoints(ui.PointMode.points, points, paint); - canvas.drawPoints(ui.PointMode.points, orion, paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; - } -} - class Meteor extends CustomPainter { Paint _paint; Meteor() { @@ -1365,3 +1300,301 @@ class _MeteorLoaderState extends State ); } } + +class SleepMode extends StatefulWidget { + SleepMode({Key key}) : super(key: key); + + @override + SleepModeState createState() => SleepModeState(); +} + +class SleepModeState extends State + with SingleTickerProviderStateMixin { + int _minSelected; + List minsToSelect = [10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99]; + AnimationController _controller; + Animation _animation; + @override + void initState() { + super.initState(); + _minSelected = 30; + _controller = + AnimationController(vsync: this, duration: Duration(milliseconds: 400)); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + Provider.of(context, listen: false) + .setSwitchValue = _animation.value; + }); + + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + Provider.of(context, listen: false) + .sleepTimer(_minSelected); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + List customShadow(double scale) => [ + BoxShadow( + blurRadius: 26 * (1 - scale), + offset: Offset(-6, -6) * (1 - scale), + color: Colors.white), + BoxShadow( + blurRadius: 8 * (1 - scale), + offset: Offset(2, 2) * (1 - scale), + color: Colors.grey[600].withOpacity(0.4)) + ]; + List customShadowNight(double scale) => [ + BoxShadow( + blurRadius: 6 * (1 - scale), + offset: Offset(-1, -1) * (1 - scale), + color: Colors.grey[100].withOpacity(0.3)), + BoxShadow( + blurRadius: 8 * (1 - scale), + offset: Offset(2, 2) * (1 - scale), + color: Colors.black) + ]; + + @override + Widget build(BuildContext context) { + final ColorTween _colorTween = + ColorTween(begin: context.primaryColor, end: Colors.black); + var audio = Provider.of(context, listen: false); + return Selector>( + selector: (_, audio) => + Tuple3(audio.timeLeft, audio.switchValue, audio.sleepTimerMode), + builder: (_, data, __) { + double fraction = data.item2 < 0.5 ? data.item2 * 2 : 1; + double move = data.item2 > 0.5 ? data.item2 * 2 - 1 : 0; + return Container( + height: 300, + color: _colorTween.transform(move), + child: Stack( + children: [ + Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.all(5), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Padding( + padding: EdgeInsets.symmetric(vertical: 20), + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: minsToSelect + .map((e) => InkWell( + onTap: () => setState(() => _minSelected = e), + child: Stack( + alignment: Alignment.center, + children: [ + Container( + margin: EdgeInsets.symmetric( + horizontal: 10.0), + decoration: BoxDecoration( + boxShadow: !(e == _minSelected || + fraction > 0) + ? (Theme.of(context).brightness == + Brightness.dark) + ? customShadowNight(fraction) + : customShadow(fraction) + : null, + color: (e == _minSelected) + ? Theme.of(context).accentColor + : Theme.of(context).primaryColor, + shape: BoxShape.circle, + ), + alignment: Alignment.center, + height: 30, + width: 30, + child: Text(e.toString(), + style: TextStyle( + fontWeight: FontWeight.bold, + color: (e == _minSelected) + ? Colors.white + : null)), + ), + Container( + height: 30 * move, + width: 30 * move, + decoration: BoxDecoration( + color: + _colorTween.transform(fraction), + shape: BoxShape.circle), + ), + ], + ), + )) + .toList(), + ), + ), + ), + Stack( + children: [ + Container( + height: 100, + alignment: Alignment.center, + ), + Positioned( + left: data.item3 == SleepTimerMode.timer + ? -context.width * (move) / 4 + : context.width * (move) / 4, + child: Container( + height: 100, + width: context.width, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Container( + alignment: Alignment.center, + height: 40, + width: 120, + decoration: BoxDecoration( + border: + Border.all(color: context.primaryColor), + boxShadow: + context.brightness == Brightness.light + ? customShadow(fraction) + : customShadowNight(fraction), + color: _colorTween.transform(move), + borderRadius: + BorderRadius.all(Radius.circular(20)), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + audio.setSleepTimerMode = + SleepTimerMode.endOfEpisode; + if (fraction == 0) { + _controller.forward(); + } else if (fraction == 1) { + _controller.reverse(); + audio.cancelTimer(); + } + }, + borderRadius: + BorderRadius.all(Radius.circular(20)), + child: SizedBox( + height: 40, + width: 120, + child: Center( + child: Text( + 'End of episode', + style: TextStyle( + // fontWeight: FontWeight.bold, + // fontSize: 20, + color: (move > 0 + ? Colors.white + : null)), + ))), + ), + ), + ), + Container( + height: 100 * (1 - fraction), + width: 2, + color: context.primaryColorDark, + ), + Container( + height: 40, + width: 120, + alignment: Alignment.center, + decoration: BoxDecoration( + border: + Border.all(color: context.primaryColor), + boxShadow: + context.brightness == Brightness.light + ? customShadow(fraction) + : customShadowNight(fraction), + color: _colorTween.transform(move), + borderRadius: + BorderRadius.all(Radius.circular(20)), + ), + child: Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + audio.setSleepTimerMode = + SleepTimerMode.timer; + if (fraction == 0) { + _controller.forward(); + } else if (fraction == 1) { + _controller.reverse(); + audio.cancelTimer(); + } + }, + borderRadius: + BorderRadius.all(Radius.circular(20)), + child: SizedBox( + height: 40, + width: 120, + child: Center( + child: Text( + data.item2 == 1 + ? _stringForSeconds( + data.item1.toDouble()) + : _stringForSeconds( + (_minSelected * 60) + .toDouble()), + style: TextStyle( + // fontWeight: FontWeight.bold, + // fontSize: 20, + color: (move > 0 + ? Colors.white + : null)), + ), + ), + ), + ), + ), + ) + ], + ), + ), + ), + ], + ), + ], + ), + Positioned( + bottom: 50 + 20 * data.item2, + left: context.width / 2 - 100, + width: 200, + child: Container( + alignment: Alignment.center, + child: Text('Good Night', + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 20, + color: Colors.white.withOpacity(fraction))), + ), + ), + Positioned( + bottom: 100 * (1 - data.item2) - 30, + left: context.width / 2 - 100, + width: 200, + child: Container( + alignment: Alignment.center, + child: Text('Sleep Timer', + style: + TextStyle(fontWeight: FontWeight.bold, fontSize: 20)), + ), + ), + data.item2 == 1 ? CustomPaint(painter: StarSky()) : Center(), + data.item2 == 1 ? MeteorLoader() : Center(), + ], + ), + ); + }, + ); + } +} diff --git a/lib/home/download_list.dart b/lib/home/download_list.dart index 34b8390..ea8495f 100644 --- a/lib/home/download_list.dart +++ b/lib/home/download_list.dart @@ -51,81 +51,81 @@ class _DownloadListState extends State { @override Widget build(BuildContext context) { return SliverPadding( - padding: EdgeInsets.all(5.0), + padding: EdgeInsets.zero, sliver: Consumer(builder: (_, downloader, __) { var tasks = downloader.episodeTasks .where((task) => task.status.value != 3) .toList(); return tasks.length > 0 - ? SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return ListTile( - onTap: () => Navigator.push( - context, - ScaleRoute( - page: EpisodeDetail( - episodeItem: tasks[index].episode, - )), - ), - title: Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Expanded( - flex: 5, - child: Container( - child: Text( - tasks[index].episode.title, - maxLines: 1, - overflow: TextOverflow.ellipsis, + ? SliverPadding( + padding: EdgeInsets.all(5.0), + sliver: SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return ListTile( + onTap: () => Navigator.push( + context, + ScaleRoute( + page: EpisodeDetail( + episodeItem: tasks[index].episode, + )), + ), + title: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Expanded( + flex: 5, + child: Container( + child: Text( + tasks[index].episode.title, + maxLines: 1, + overflow: TextOverflow.ellipsis, + ), ), ), - ), - - Expanded( - flex: 1, - child: tasks[index].progress >= 0 - ? Container( - width: 40.0, - padding: - EdgeInsets.symmetric(horizontal: 2), - alignment: Alignment.center, - decoration: BoxDecoration( - borderRadius: BorderRadius.all( - Radius.circular(6)), - color: Colors.red), - child: Text( - tasks[index].progress.toString() + '%', - style: TextStyle(color: Colors.white), - )) - : Container(), - ), - ], - ), - subtitle: SizedBox( - height: 2, - child: LinearProgressIndicator( - value: tasks[index].progress / 100, + Expanded( + flex: 1, + child: tasks[index].progress >= 0 + ? Container( + width: 40.0, + padding: + EdgeInsets.symmetric(horizontal: 2), + alignment: Alignment.center, + decoration: BoxDecoration( + borderRadius: BorderRadius.all( + Radius.circular(6)), + color: Colors.red), + child: Text( + tasks[index].progress.toString() + '%', + style: TextStyle(color: Colors.white), + )) + : Container(), + ), + ], ), - ), - leading: CircleAvatar( - backgroundImage: FileImage( - File("${tasks[index].episode.imagePath}")), - ), - trailing: _downloadButton(tasks[index], context), - ); - }, - childCount: tasks.length, + subtitle: SizedBox( + height: 2, + child: LinearProgressIndicator( + value: tasks[index].progress / 100, + ), + ), + leading: CircleAvatar( + backgroundImage: FileImage( + File("${tasks[index].episode.imagePath}")), + ), + trailing: Container( + width: 50, + height: 50, + child: _downloadButton(tasks[index], context)), + ); + }, + childCount: tasks.length, + ), ), ) - : SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return Center(); - }, - childCount: 1, - ), + : SliverToBoxAdapter( + child: Center(), ); }), ); diff --git a/lib/home/home_groups.dart b/lib/home/home_groups.dart index 9a69ff8..606c6ab 100644 --- a/lib/home/home_groups.dart +++ b/lib/home/home_groups.dart @@ -18,6 +18,7 @@ import 'package:tsacdop/podcasts/podcastdetail.dart'; import 'package:tsacdop/podcasts/podcastmanage.dart'; import 'package:tsacdop/util/pageroute.dart'; import 'package:tsacdop/util/colorize.dart'; +import 'package:tsacdop/util/context_extension.dart'; class ScrollPodcasts extends StatefulWidget { @override @@ -231,6 +232,7 @@ class _ScrollPodcastsState extends State { height: 70, width: _width, alignment: Alignment.centerLeft, + color: context.scaffoldBackgroundColor, child: TabBar( labelPadding: EdgeInsets.only( top: 5.0, @@ -243,7 +245,7 @@ class _ScrollPodcastsState extends State { isScrollable: true, tabs: groups[_groupIndex] .podcasts - .map((PodcastLocal podcastLocal) { + .map((PodcastLocal podcastLocal) { return Tab( child: ClipRRect( borderRadius: BorderRadius.all( @@ -403,6 +405,10 @@ class ShowEpisode extends StatelessWidget { final List episodes; final PodcastLocal podcastLocal; ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key); + String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } @override Widget build(BuildContext context) { @@ -597,17 +603,36 @@ class ShowEpisode extends StatelessWidget { ? Container( alignment: Alignment.center, child: Text( - (episodes[index].duration) + _stringForSeconds( + episodes[index] + .duration + .toDouble()) .toString() + - 'mins', + '|', style: TextStyle( fontSize: _width / 35, - // color: _c, - // fontStyle: FontStyle.italic, + // color: _c, + // fontStyle: FontStyle.italic, ), ), ) : Center(), + episodes[index].enclosureLength != null && + episodes[index].enclosureLength != + 0 + ? Container( + alignment: Alignment.center, + child: Text( + ((episodes[index] + .enclosureLength) ~/ + 1000000) + .toString() + + 'MB', + style: TextStyle( + fontSize: _width / 35), + ), + ) + : Center(), ], )), ], diff --git a/lib/home/nested_home.dart b/lib/home/nested_home.dart index b3f153d..f566ef1 100644 --- a/lib/home/nested_home.dart +++ b/lib/home/nested_home.dart @@ -4,15 +4,19 @@ import 'dart:io'; import 'package:flutter/material.dart' hide NestedScrollView; import 'package:provider/provider.dart'; import 'package:tsacdop/class/download_state.dart'; +import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/home/playlist.dart'; import 'package:tuple/tuple.dart'; import 'package:extended_nested_scroll_view/extended_nested_scroll_view.dart'; +import 'package:line_icons/line_icons.dart'; import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import 'package:tsacdop/util/episodegrid.dart'; import 'package:tsacdop/util/mypopupmenu.dart'; +import 'package:tsacdop/util/context_extension.dart'; +import 'package:tsacdop/util/custompaint.dart'; import 'package:tsacdop/home/appbar/importompl.dart'; import 'package:tsacdop/home/audioplayer.dart'; @@ -328,10 +332,14 @@ class _RecentUpdate extends StatefulWidget { } class _RecentUpdateState extends State<_RecentUpdate> - with AutomaticKeepAliveClientMixin { - Future> _getRssItem(int top) async { + with AutomaticKeepAliveClientMixin , SingleTickerProviderStateMixin{ + Future> _getRssItem(int top, List group) async { var dbHelper = DBHelper(); - List episodes = await dbHelper.getRecentRssItem(top); + List episodes; + if (group.first == 'All') + episodes = await dbHelper.getRecentRssItem(top); + else + episodes = await dbHelper.getGroupRssItem(top, group); return episodes; } @@ -347,18 +355,23 @@ class _RecentUpdateState extends State<_RecentUpdate> int _top = 99; bool _loadMore; - + String _groupName; + List _group; + Layout _layout; @override void initState() { super.initState(); _loadMore = false; + _groupName = 'All'; + _group = ['All']; + _layout = Layout.three; } @override Widget build(BuildContext context) { super.build(context); return FutureBuilder>( - future: _getRssItem(_top), + future: _getRssItem(_top, _group), builder: (context, snapshot) { if (snapshot.hasError) print(snapshot.error); return (snapshot.hasData) @@ -373,8 +386,112 @@ class _RecentUpdateState extends State<_RecentUpdate> key: PageStorageKey('update'), physics: const AlwaysScrollableScrollPhysics(), slivers: [ + SliverToBoxAdapter( + child: Container( + height: 40, + color: context.primaryColor, + child: Row( + children: [ + Consumer( + builder: (context, groupList, child) => + Material( + color: Colors.transparent, + child: PopupMenuButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10))), + elevation: 1, + tooltip: 'Groups fliter', + child: Container( + padding: EdgeInsets.symmetric( + horizontal: 20), + height: 50, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text(_groupName), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5), + ), + Icon( + LineIcons.filter_solid, + size: 18, + ) + ], + )), + itemBuilder: (context) => [ + PopupMenuItem( + child: Text('All'), value: 'All') + ]..addAll(groupList.groups + .map>((e) => + PopupMenuItem( + value: e.name, + child: Text(e.name))) + .toList()), + onSelected: (value) { + if (value == 'All') { + setState(() { + _groupName = 'All'; + _group = ['All']; + }); + } else { + groupList.groups.forEach((group) { + if (group.name == value) { + setState(() { + _groupName = value; + _group = group.podcastList; + }); + } + }); + } + }, + ), + ), + ), + Spacer(), + Material( + color: Colors.transparent, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + if (_layout == Layout.three) + setState(() { + _layout = Layout.two; + }); + else + setState(() { + _layout = Layout.three; + }); + }, + icon: _layout == Layout.three + ? SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 0, + context.textTheme.bodyText1 + .color), + ), + ) + : SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 1, + context.textTheme.bodyText1 + .color), + ), + ), + )), + ], + )), + ), EpisodeGrid( episodes: snapshot.data, + layout: _layout, ), SliverList( delegate: SliverChildBuilderDelegate( @@ -405,9 +522,9 @@ class _MyFavorite extends StatefulWidget { class _MyFavoriteState extends State<_MyFavorite> with AutomaticKeepAliveClientMixin { - Future> _getLikedRssItem(_top) async { + Future> _getLikedRssItem(int top, int sortBy) async { var dbHelper = DBHelper(); - List episodes = await dbHelper.getLikedRssItem(_top); + List episodes = await dbHelper.getLikedRssItem(top, sortBy); return episodes; } @@ -423,18 +540,21 @@ class _MyFavoriteState extends State<_MyFavorite> int _top = 99; bool _loadMore; - + Layout _layout; + int _sortBy; @override void initState() { super.initState(); _loadMore = false; + _layout = Layout.three; + _sortBy = 0; } @override Widget build(BuildContext context) { super.build(context); return FutureBuilder>( - future: _getLikedRssItem(_top), + future: _getLikedRssItem(_top, _sortBy), builder: (context, snapshot) { if (snapshot.hasError) print(snapshot.error); return (snapshot.hasData) @@ -448,8 +568,103 @@ class _MyFavoriteState extends State<_MyFavorite> child: CustomScrollView( key: PageStorageKey('favorite'), slivers: [ + SliverToBoxAdapter( + child: Container( + height: 40, + color: context.primaryColor, + child: Row( + children: [ + Material( + color: Colors.transparent, + child: PopupMenuButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10))), + elevation: 1, + tooltip: 'Sort By', + child: Container( + height: 50, + padding: + EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Sory by'), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5), + ), + Icon( + _sortBy == 0 + ? LineIcons + .cloud_download_alt_solid + : LineIcons.heartbeat_solid, + size: 18, + ) + ], + )), + itemBuilder: (context) => [ + PopupMenuItem( + value: 0, + child: Text('Update Date'), + ), + PopupMenuItem( + value: 1, + child: Text('Like Date'), + ) + ], + onSelected: (value) { + if (value == 0) + setState(() => _sortBy = 0); + else if (value == 1) + setState(() => _sortBy = 1); + }, + ), + ), + Spacer(), + Material( + color: Colors.transparent, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + if (_layout == Layout.three) + setState(() { + _layout = Layout.two; + }); + else + setState(() { + _layout = Layout.three; + }); + }, + icon: _layout == Layout.three + ? SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 0, + context + .textTheme.bodyText1.color), + ), + ) + : SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 1, + context + .textTheme.bodyText1.color), + ), + ), + ), + ), + ], + )), + ), EpisodeGrid( episodes: snapshot.data, + layout: _layout, ), SliverList( delegate: SliverChildBuilderDelegate( @@ -481,24 +696,65 @@ class _MyDownload extends StatefulWidget { class _MyDownloadState extends State<_MyDownload> with AutomaticKeepAliveClientMixin { + Layout _layout; + @override + void initState() { + super.initState(); + _layout = Layout.three; + } + @override Widget build(BuildContext context) { super.build(context); return CustomScrollView( - key: PageStorageKey('downloas_list'), + key: PageStorageKey('download_list'), slivers: [ DownloadList(), - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return Container( - alignment: Alignment.centerLeft, - padding: EdgeInsets.all(15.0), - child: Text('Downloaded'), - ); - }, - childCount: 1, - ), + SliverToBoxAdapter( + child: Container( + height: 40, + color: context.primaryColor, + child: Row( + children: [ + Container( + padding: EdgeInsets.symmetric(horizontal: 20), + child: Text('Downloaded')), + Spacer(), + Material( + color: Colors.transparent, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + if (_layout == Layout.three) + setState(() { + _layout = Layout.two; + }); + else + setState(() { + _layout = Layout.three; + }); + }, + icon: _layout == Layout.three + ? SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 0, context.textTheme.bodyText1.color), + ), + ) + : SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 1, context.textTheme.bodyText1.color), + ), + ), + ), + ), + ], + )), ), Consumer( builder: (_, downloader, __) { @@ -511,6 +767,7 @@ class _MyDownloadState extends State<_MyDownload> .toList(); return EpisodeGrid( episodes: episodes, + layout: _layout, ); }, ), @@ -520,4 +777,4 @@ class _MyDownloadState extends State<_MyDownload> @override bool get wantKeepAlive => true; -} +} \ No newline at end of file diff --git a/lib/home/playlist.dart b/lib/home/playlist.dart index fb7d5a8..fba9df8 100644 --- a/lib/home/playlist.dart +++ b/lib/home/playlist.dart @@ -4,7 +4,6 @@ import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tuple/tuple.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:line_icons/line_icons.dart'; @@ -12,7 +11,7 @@ import 'package:line_icons/line_icons.dart'; import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/util/context_extension.dart'; -import 'package:tsacdop/home/audioplayer.dart'; +import 'package:tsacdop/util/custompaint.dart'; class PlaylistPage extends StatefulWidget { @override @@ -28,7 +27,7 @@ class _PlaylistPageState extends State { return sum; } else { episodes.forEach((episode) { - sum += episode.duration; + sum += episode.duration ~/ 60; }); return sum; } @@ -78,7 +77,6 @@ class _PlaylistPageState extends State { selector: (_, audio) => Tuple3(audio.queue, audio.playerRunning, audio.queueUpdate), builder: (_, data, __) { - print('update'); final List episodes = data.item1.playlist; return Column( mainAxisSize: MainAxisSize.min, @@ -150,15 +148,17 @@ class _PlaylistPageState extends State { child: Container( padding: EdgeInsets.all(5.0), margin: EdgeInsets.only(right: 20.0, bottom: 5.0), - decoration: data.item2 ? BoxDecoration( - color: context.brightness == Brightness.dark ? Colors.grey[800] : Colors.grey[200], - borderRadius: - BorderRadius.all(Radius.circular(10.0)), - ) : - BoxDecoration( - shape: BoxShape.circle, - color: Colors.transparent - ), + decoration: data.item2 + ? BoxDecoration( + color: context.brightness == Brightness.dark + ? Colors.grey[800] + : Colors.grey[200], + borderRadius: + BorderRadius.all(Radius.circular(10.0)), + ) + : BoxDecoration( + shape: BoxShape.circle, + color: Colors.transparent), child: data.item2 ? _topHeight < 90 ? Row( @@ -178,7 +178,7 @@ class _PlaylistPageState extends State { child: SizedBox( width: 20, height: 15, - child: WaveLoader()), + child: WaveLoader(color: context.accentColor,)), ), ], ) @@ -210,13 +210,13 @@ class _PlaylistPageState extends State { child: SizedBox( width: 20, height: 15, - child: WaveLoader()), + child: WaveLoader(color: context.accentColor,)), ), ], ) : IconButton( - padding: EdgeInsets.all(0), - alignment: Alignment.center, + padding: EdgeInsets.all(0), + alignment: Alignment.center, icon: Icon(Icons.play_circle_filled, size: 40, color: Theme.of(context).accentColor), @@ -360,7 +360,8 @@ class _DismissibleContainerState extends State { ); Scaffold.of(context).showSnackBar(SnackBar( backgroundColor: Colors.grey[800], - content: Text('Episode removed', style: TextStyle(color: Colors.white)), + content: Text('Episode removed', + style: TextStyle(color: Colors.white)), action: SnackBarAction( textColor: context.accentColor, label: 'Undo', @@ -403,7 +404,8 @@ class _DismissibleContainerState extends State { : Center(), widget.episode.duration != 0 ? _episodeTag( - (widget.episode.duration).toString() + 'mins', + (widget.episode.duration ~/ 60).toString() + + 'mins', Colors.cyan[300]) : Center(), widget.episode.enclosureLength != null diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index dfc240d..aef2df0 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -21,7 +21,8 @@ class DBHelper { initDb() async { var documentsDirectory = await getDatabasesPath(); String path = join(documentsDirectory, "podcasts.db"); - Database theDb = await openDatabase(path, version: 1, onCreate: _onCreate); + Database theDb = await openDatabase(path, + version: 1, onCreate: _onCreate, onUpgrade: _onUpgrade); return theDb; } @@ -37,16 +38,24 @@ 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, media_id TEXT, + liked_date INTEGER DEFAULT 0, downloaded TEXT DEFAULT 'ND', download_date INTEGER DEFAULT 0, media_id TEXT, is_new INTEGER DEFAULT 0)"""); await db.execute( - """CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT UNIQUE, - seconds REAL, seek_value REAL, add_date INTEGER)"""); + """CREATE TABLE PlayHistory(id INTEGER PRIMARY KEY, title TEXT, enclosure_url TEXT, + seconds REAL, seek_value REAL, add_date INTEGER, listen_time INTEGER DEFAULT 0)"""); 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)"""); } + void _onUpgrade(Database db, int oldVersion, int newVersion) async { + if (oldVersion == 1) { + await db.execute("ALTER TABLE Episodes ADD liked_date INTEGER DEFAULT 0"); + await db + .execute("ALTER TABLE PlayHistory ADD listen_time INTEGER DEFAULT 0"); + } + } + Future> getPodcastLocal(List podcasts) async { var dbClient = await database; List podcastLocal = List(); @@ -180,16 +189,24 @@ class DBHelper { Future saveHistory(PlayHistory history) async { var dbClient = await database; int _milliseconds = DateTime.now().millisecondsSinceEpoch; + List recent = await getPlayHistory(1); + if (recent.length == 1) { + if (recent.first.url == history.url) { + await dbClient.rawDelete("DELETE FROM PlayHistory WHERE add_date = ?", + [recent.first.playdate.millisecondsSinceEpoch]); + } + } int result = await dbClient.transaction((txn) async { return await txn.rawInsert( - """REPLACE INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date) - VALUES (?, ?, ?, ?, ?) """, + """REPLACE INTO PlayHistory (title, enclosure_url, seconds, seek_value, add_date, listen_time) + VALUES (?, ?, ?, ?, ?, ?) """, [ history.title, history.url, history.seconds, history.seekValue, - _milliseconds + _milliseconds, + history.seekValue > 0.95 ? 1 : 0 ]); }); return result; @@ -200,7 +217,7 @@ class DBHelper { List list = await dbClient.rawQuery( """SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory ORDER BY add_date DESC LIMIT ? - """,[top]); + """, [top]); List playHistory = []; list.forEach((record) { playHistory.add(PlayHistory(record['title'], record['enclosure_url'], @@ -210,6 +227,23 @@ class DBHelper { return playHistory; } + Future isListened(String url) async { + var dbClient = await database; + int i = 0; + List list = + await dbClient.rawQuery("""SELECT listen_time FROM PlayHistory + WHERE enclosure_url = ? + """, [url]); + if (list.length == 0) + return 0; + else { + list.forEach((element) { + i += element['listen_time']; + }); + return i; + } + } + Future> getSubHistory() async { var dbClient = await database; List list = await dbClient.rawQuery( @@ -249,7 +283,8 @@ class DBHelper { Future getPosition(EpisodeBrief episodeBrief) async { var dbClient = await database; List list = await dbClient.rawQuery( - "SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory Where enclosure_url = ?", + """SELECT title, enclosure_url, seconds, seek_value, add_date FROM PlayHistory + WHERE enclosure_url = ? ORDER BY add_date DESC LIMIT 1""", [episodeBrief.enclosureUrl]); return list.length > 0 ? PlayHistory(list.first['title'], list.first['enclosure_url'], @@ -358,7 +393,7 @@ class DBHelper { print(pubDate); final date = _parsePubDate(pubDate); final milliseconds = date.millisecondsSinceEpoch; - final duration = feed.items[i].itunes.duration?.inMinutes ?? 0; + final duration = feed.items[i].itunes.duration?.inSeconds ?? 0; final explicit = _getExplicit(feed.items[i].itunes.explicit); if (url != null) { @@ -420,7 +455,7 @@ class DBHelper { final pubDate = feed.items[i].pubDate; final date = _parsePubDate(pubDate); final milliseconds = date.millisecondsSinceEpoch; - final duration = feed.items[i].itunes.duration?.inMinutes ?? 0; + final duration = feed.items[i].itunes.duration?.inSeconds ?? 0; final explicit = _getExplicit(feed.items[i].itunes.explicit); if (url != null) { @@ -452,30 +487,55 @@ class DBHelper { return countUpdate - count; } - Future> getRssItem(String id, int i) async { + Future> getRssItem(String id, int i, bool reverse) async { var dbClient = await database; List episodes = []; - List list = await dbClient - .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, + if (reverse) { + List list = await dbClient + .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, + E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked, + E.downloaded, P.primaryColor , E.media_id, E.is_new + FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + WHERE P.id = ? ORDER BY E.milliseconds ASC LIMIT ?""", [id, i]); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['milliseconds'], + list[x]['feedTitle'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'], + list[x]['imagePath'], + list[x]['media_id'], + list[x]['is_new'])); + } + } else { + List list = await dbClient + .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, P.title as feedTitle, E.duration, E.explicit, E.liked, E.downloaded, P.primaryColor , E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id WHERE P.id = ? ORDER BY E.milliseconds DESC LIMIT ?""", [id, i]); - for (int x = 0; x < list.length; x++) { - episodes.add(EpisodeBrief( - list[x]['title'], - list[x]['enclosure_url'], - list[x]['enclosure_length'], - list[x]['milliseconds'], - list[x]['feedTitle'], - list[x]['primaryColor'], - list[x]['liked'], - list[x]['downloaded'], - list[x]['duration'], - list[x]['explicit'], - list[x]['imagePath'], - list[x]['media_id'], - list[x]['is_new'])); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['milliseconds'], + list[x]['feedTitle'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'], + list[x]['imagePath'], + list[x]['media_id'], + list[x]['is_new'])); + } } return episodes; } @@ -565,38 +625,96 @@ class DBHelper { return episodes; } - Future> getLikedRssItem(int i) async { + Future> getGroupRssItem( + int top, List group) async { + var dbClient = await database; + List episodes = []; + if (group.length > 0) { + List s = group.map((e) => "'$e'").toList(); + List list = await dbClient + .rawQuery("""SELECT E.title, E.enclosure_url, E.enclosure_length, + E.milliseconds, P.title as feed_title, E.duration, E.explicit, E.liked, + E.downloaded, P.imagePath, P.primaryColor, E.media_id, E.is_new + FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + WHERE P.id in (${s.join(',')}) + ORDER BY E.milliseconds DESC LIMIT ? """, [top]); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['milliseconds'], + list[x]['feed_title'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['doanloaded'], + list[x]['duration'], + list[x]['explicit'], + list[x]['imagePath'], + list[x]['media_id'], + list[x]['is_new'])); + } + } + return episodes; + } + + Future> getLikedRssItem(int i, int sortBy) 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.imagePath, + if (sortBy == 0) { + List list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id WHERE E.liked = 1 ORDER BY E.milliseconds DESC LIMIT ?""", [i]); - for (int x = 0; x < list.length; x++) { - episodes.add(EpisodeBrief( - list[x]['title'], - list[x]['enclosure_url'], - list[x]['enclosure_length'], - list[x]['milliseconds'], - list[x]['feed_title'], - list[x]['primaryColor'], - list[x]['liked'], - list[x]['downloaded'], - list[x]['duration'], - list[x]['explicit'], - list[x]['imagePath'], - list[x]['media_id'], - list[x]['is_new'])); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['milliseconds'], + list[x]['feed_title'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'], + list[x]['imagePath'], + list[x]['media_id'], + list[x]['is_new'])); + } + } else { + List list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, E.milliseconds, P.imagePath, + P.title as feed_title, E.duration, E.explicit, E.liked, E.downloaded, + P.primaryColor, E.media_id, E.is_new FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + WHERE E.liked = 1 ORDER BY E.liked_date DESC LIMIT ?""", [i]); + for (int x = 0; x < list.length; x++) { + episodes.add(EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + list[x]['enclosure_length'], + list[x]['milliseconds'], + list[x]['feed_title'], + list[x]['primaryColor'], + list[x]['liked'], + list[x]['downloaded'], + list[x]['duration'], + list[x]['explicit'], + list[x]['imagePath'], + list[x]['media_id'], + list[x]['is_new'])); + } } return episodes; } Future setLiked(String url) async { var dbClient = await database; + int milliseconds = DateTime.now().millisecondsSinceEpoch; int count = await dbClient.rawUpdate( - "UPDATE Episodes SET liked = 1 WHERE enclosure_url= ?", [url]); - print('liked'); + "UPDATE Episodes SET liked = 1, liked_date = ? WHERE enclosure_url= ?", + [milliseconds, url]); return count; } @@ -604,10 +722,16 @@ class DBHelper { var dbClient = await database; int count = await dbClient.rawUpdate( "UPDATE Episodes SET liked = 0 WHERE enclosure_url = ?", [url]); - print('unliked'); return count; } + Future isLiked(String url) async { + var dbClient = await database; + List list = await dbClient + .rawQuery("SELECT liked FROM Episodes WHERE enclosure_url = ?", [url]); + return list.first['liked'] == 0 ? false : true; + } + Future saveDownloaded(String url, String id) async { var dbClient = await database; int milliseconds = DateTime.now().millisecondsSinceEpoch; diff --git a/lib/podcasts/podcastdetail.dart b/lib/podcasts/podcastdetail.dart index 7a51e29..ca3baf9 100644 --- a/lib/podcasts/podcastdetail.dart +++ b/lib/podcasts/podcastdetail.dart @@ -11,6 +11,7 @@ import 'package:url_launcher/url_launcher.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:provider/provider.dart'; +import 'package:line_icons/line_icons.dart'; import 'package:cached_network_image/cached_network_image.dart'; import 'package:tsacdop/class/podcastlocal.dart'; @@ -20,6 +21,8 @@ import 'package:tsacdop/util/episodegrid.dart'; import 'package:tsacdop/home/audioplayer.dart'; import 'package:tsacdop/class/fireside_data.dart'; import 'package:tsacdop/util/colorize.dart'; +import 'package:tsacdop/util/context_extension.dart'; +import 'package:tsacdop/util/custompaint.dart'; class PodcastDetail extends StatefulWidget { PodcastDetail({Key key, this.podcastLocal}) : super(key: key); @@ -36,25 +39,27 @@ class _PodcastDetailState extends State { Future _updateRssItem(PodcastLocal podcastLocal) async { var dbHelper = DBHelper(); final result = await dbHelper.updatePodcastRss(podcastLocal); - if(result == 0) - { Fluttertoast.showToast( - msg: 'No Update', - gravity: ToastGravity.TOP, - );} - else{ - Fluttertoast.showToast( - msg: 'Updated $result Episodes', - gravity: ToastGravity.TOP, - ); - Provider.of(context, listen: false).updatePodcast(podcastLocal); - } + if (result == 0) { + Fluttertoast.showToast( + msg: 'No Update', + gravity: ToastGravity.TOP, + ); + } else { + Fluttertoast.showToast( + msg: 'Updated $result Episodes', + gravity: ToastGravity.TOP, + ); + Provider.of(context, listen: false) + .updatePodcast(podcastLocal); + } if (mounted) setState(() {}); } Future> _getRssItem( - PodcastLocal podcastLocal, int i) async { + PodcastLocal podcastLocal, int i, bool reverse) async { var dbHelper = DBHelper(); - List episodes = await dbHelper.getRssItem(podcastLocal.id, i); + List episodes = + await dbHelper.getRssItem(podcastLocal.id, i, reverse); if (podcastLocal.provider.contains('fireside')) { FiresideData data = FiresideData(podcastLocal.id, podcastLocal.link); await data.getData(); @@ -168,12 +173,15 @@ class _PodcastDetailState extends State { ScrollController _controller; int _top; bool _loadMore; - + Layout _layout; + bool _reverse; @override void initState() { super.initState(); _loadMore = false; _top = 99; + _layout = Layout.three; + _reverse = false; _controller = ScrollController(); } @@ -210,7 +218,8 @@ class _PodcastDetailState extends State { children: [ Expanded( child: FutureBuilder>( - future: _getRssItem(widget.podcastLocal, _top), + future: + _getRssItem(widget.podcastLocal, _top, _reverse), builder: (context, snapshot) { if (snapshot.hasError) print(snapshot.error); return (snapshot.hasData) @@ -232,7 +241,8 @@ class _PodcastDetailState extends State { }); } }), - physics: const AlwaysScrollableScrollPhysics(), + physics: + const AlwaysScrollableScrollPhysics(), //primary: true, slivers: [ SliverAppBar( @@ -392,18 +402,121 @@ class _PodcastDetailState extends State { ); }), ), - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return hostsList(context, hosts); - }, - childCount: 1, - ), + SliverToBoxAdapter( + child: hostsList(context, hosts), + ), + SliverToBoxAdapter( + child: Container( + height: 30, + child: Row( + children: [ + Material( + color: Colors.transparent, + child: PopupMenuButton( + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.all( + Radius.circular( + 10))), + elevation: 1, + tooltip: 'Sort By', + child: Container( + height: 30, + padding: + EdgeInsets.symmetric( + horizontal: 15), + child: Row( + mainAxisSize: + MainAxisSize.min, + children: [ + Text('Sort by'), + Padding( + padding: EdgeInsets + .symmetric( + horizontal: + 5), + ), + Icon( + _reverse + ? LineIcons + .hourglass_start_solid + : LineIcons + .hourglass_end_solid, + size: 18, + ) + ], + )), + itemBuilder: (context) => [ + PopupMenuItem( + value: 0, + child: Text('Newest first'), + ), + PopupMenuItem( + value: 1, + child: Text('Oldest first'), + ) + ], + onSelected: (value) { + if (value == 0) + setState(() => + _reverse = false); + else if (value == 1) + setState(() => + _reverse = true); + }, + ), + ), + Spacer(), + Material( + color: Colors.transparent, + child: IconButton( + padding: EdgeInsets.zero, + onPressed: () { + if (_layout == Layout.three) + setState(() { + _layout = Layout.two; + }); + else + setState(() { + _layout = Layout.three; + }); + }, + icon: _layout == Layout.three + ? SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 0, + context + .textTheme + .bodyText1 + .color), + ), + ) + : SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 1, + context + .textTheme + .bodyText1 + .color), + ), + ), + ), + ), + ], + )), ), EpisodeGrid( episodes: snapshot.data, showFavorite: true, showNumber: true, + layout: _layout, + reverse: _reverse, episodeCount: widget.podcastLocal.episodeCount, ), @@ -519,12 +632,6 @@ class _AboutPodcastState extends State { maxLines: 3, overflow: TextOverflow.ellipsis, ), - Container( - alignment: Alignment.center, - child: Icon( - Icons.keyboard_arrow_down, - ), - ), ], ) : Linkify( @@ -554,3 +661,4 @@ class _AboutPodcastState extends State { ); } } + diff --git a/lib/podcasts/podcastlist.dart b/lib/podcasts/podcastlist.dart index d302227..0330e7d 100644 --- a/lib/podcasts/podcastlist.dart +++ b/lib/podcasts/podcastlist.dart @@ -162,12 +162,6 @@ class _PodcastListState extends State { 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: AboutPodcast( podcastLocal: diff --git a/lib/podcasts/podcastmanage.dart b/lib/podcasts/podcastmanage.dart index 8ea9026..4efff03 100644 --- a/lib/podcasts/podcastmanage.dart +++ b/lib/podcasts/podcastmanage.dart @@ -10,6 +10,7 @@ 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 'package:tsacdop/util/context_extension.dart'; import 'custom_tabview.dart'; class PodcastManage extends StatefulWidget { @@ -173,31 +174,34 @@ class _PodcastManageState extends State ? Center() : Stack( children: [ - CustomTabView( - itemCount: _groups.length, - tabBuilder: (context, index) => Tab( - child: Container( - height: 30.0, - padding: EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.center, - decoration: BoxDecoration( - color: (_scroll - index).abs() > 1 - ? Colors.grey[300] - : Colors.grey[300] - .withOpacity((_scroll - index).abs()), - borderRadius: - BorderRadius.all(Radius.circular(15)), - ), - child: Text( - _groups[index].name, - )), + Container( + color: context.scaffoldBackgroundColor, + child: CustomTabView( + itemCount: _groups.length, + tabBuilder: (context, index) => Tab( + child: Container( + height: 30.0, + padding: EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.center, + decoration: BoxDecoration( + color: (_scroll - index).abs() > 1 + ? Colors.grey[300] + : Colors.grey[300] + .withOpacity((_scroll - index).abs()), + borderRadius: + BorderRadius.all(Radius.circular(15)), + ), + child: Text( + _groups[index].name, + )), + ), + pageBuilder: (context, index) => Container( + key: ValueKey(_groups[index].name), + child: PodcastGroupList(group: _groups[index])), + onPositionChange: (value) => + setState(() => _index = value), + onScroll: (value) => setState(() => _scroll = value), ), - pageBuilder: (context, index) => Container( - key: ValueKey(_groups[index].name), - child: PodcastGroupList(group: _groups[index])), - onPositionChange: (value) => - setState(() => _index = value), - onScroll: (value) => setState(() => _scroll = value), ), _showSetting ? Positioned.fill( diff --git a/lib/settings/history.dart b/lib/settings/history.dart index c29fd2f..4ce12f0 100644 --- a/lib/settings/history.dart +++ b/lib/settings/history.dart @@ -285,7 +285,7 @@ class _PlayedHistoryState extends State .data[index].subDate) .inDays .toString() + - ' days') + ' days ago') : Text(snapshot.data[index].delDate .difference(snapshot .data[index].subDate) diff --git a/lib/settings/licenses.dart b/lib/settings/licenses.dart index ca90500..676a196 100644 --- a/lib/settings/licenses.dart +++ b/lib/settings/licenses.dart @@ -46,4 +46,5 @@ List plugins = [ Libries('flutter_linkify', mit, 'https://pub.dev/packages/flutter_linkify'), Libries('extended_nested_scroll_view', mit, 'https://pub.dev/packages/extended_nested_scroll_view'), Libries('connectivity', bsd, 'https://pub.dev/packages/connectivity'), + Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart') ]; \ No newline at end of file diff --git a/lib/util/custompaint.dart b/lib/util/custompaint.dart new file mode 100644 index 0000000..9993935 --- /dev/null +++ b/lib/util/custompaint.dart @@ -0,0 +1,476 @@ +import 'dart:io'; +import 'dart:ui' as ui; +import 'dart:math' as math; +import 'package:flutter/material.dart'; + +//Layout change indicator +class LayoutPainter extends CustomPainter { + double scale; + Color color; + LayoutPainter(this.scale, this.color); + @override + void paint(Canvas canvas, Size size) { + Paint _paint = Paint() + ..color = color + ..strokeWidth = 1.0 + ..style = PaintingStyle.stroke + ..strokeCap = StrokeCap.round; + + canvas.drawRect(Rect.fromLTRB(0, 0, 10 + 5 * scale, 10), _paint); + canvas.drawRect( + Rect.fromLTRB(10 + 5 * scale, 0, 20 + 10 * scale, 10), _paint); + canvas.drawRect( + Rect.fromLTRB(20 + 5 * scale, 0, 30, 10 - 10 * scale), _paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} + +//Dark sky used in sleep timer +class StarSky extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + final points = [ + Offset(50, 100), + Offset(150, 75), + Offset(250, 250), + Offset(130, 200), + Offset(270, 150), + ]; + final pisces = [ + Offset(9, 4), + Offset(11, 5), + Offset(7, 6), + Offset(10, 7), + Offset(8, 8), + Offset(9, 13), + Offset(12, 17), + Offset(5, 19), + Offset(7, 19) + ].map((e) => e * 10).toList(); + final orion = [ + Offset(3, 1), + Offset(6, 1), + Offset(1, 4), + Offset(2, 4), + Offset(2, 7), + Offset(10, 8), + Offset(3, 10), + Offset(8, 10), + Offset(19, 11), + Offset(11, 13), + Offset(18, 14), + Offset(5, 19), + Offset(7, 19), + Offset(9, 18), + Offset(15, 19), + Offset(16, 18), + Offset(2, 25), + Offset(10, 26) + ].map((e) => Offset(e.dx * 10 + 250, e.dy * 10)).toList(); + + Paint paint = Paint() + ..color = Colors.white + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round; + canvas.drawPoints(ui.PointMode.points, pisces, paint); + canvas.drawPoints(ui.PointMode.points, points, paint); + canvas.drawPoints(ui.PointMode.points, orion, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} + +//Listened indicator +class ListenedPainter extends CustomPainter { + Color _color; + double stroke; + ListenedPainter(this._color,{this.stroke = 1.0}); + @override + void paint(Canvas canvas, Size size) { + Paint _paint = Paint() + ..color = _color + ..strokeWidth = stroke + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + Path _path = Path(); + + _path.moveTo(size.width / 6, size.height * 3 / 8); + _path.lineTo(size.width / 6, size.height * 5 / 8); + _path.moveTo(size.width / 3, size.height / 4); + _path.lineTo(size.width / 3, size.height * 3 / 4); + _path.moveTo(size.width / 2, size.height / 8); + _path.lineTo(size.width / 2, size.height * 7 / 8); + _path.moveTo(size.width * 5 / 6, size.height * 3 / 8); + _path.lineTo(size.width * 5 / 6, size.height * 5 / 8); + _path.moveTo(size.width * 2 / 3, size.height / 4); + _path.lineTo(size.width * 2 / 3, size.height * 3 / 4); + + canvas.drawPath(_path, _paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} + +//Wave play indicator +class WavePainter extends CustomPainter { + double _fraction; + double _value; + Color _color; + WavePainter(this._fraction, this._color); + @override + void paint(Canvas canvas, Size size) { + if (_fraction < 0.5) { + _value = _fraction; + } else { + _value = 1 - _fraction; + } + Path _path = Path(); + Paint _paint = Paint() + ..color = _color + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round + ..style = PaintingStyle.stroke; + _path.moveTo(0, size.height / 2); + _path.lineTo(0, size.height / 2 + size.height * _value * 0.2); + _path.moveTo(0, size.height / 2); + _path.lineTo(0, size.height / 2 - size.height * _value * 0.2); + _path.moveTo(size.width / 4, size.height / 2); + _path.lineTo(size.width / 4, size.height / 2 + size.height * _value * 0.8); + _path.moveTo(size.width / 4, size.height / 2); + _path.lineTo(size.width / 4, size.height / 2 - size.height * _value * 0.8); + _path.moveTo(size.width / 2, size.height / 2); + _path.lineTo(size.width / 2, size.height / 2 + size.height * _value * 0.5); + _path.moveTo(size.width / 2, size.height / 2); + _path.lineTo(size.width / 2, size.height / 2 - size.height * _value * 0.5); + _path.moveTo(size.width * 3 / 4, size.height / 2); + _path.lineTo( + size.width * 3 / 4, size.height / 2 + size.height * _value * 0.6); + _path.moveTo(size.width * 3 / 4, size.height / 2); + _path.lineTo( + size.width * 3 / 4, size.height / 2 - size.height * _value * 0.6); + _path.moveTo(size.width, size.height / 2); + _path.lineTo(size.width, size.height / 2 + size.height * _value * 0.2); + _path.moveTo(size.width, size.height / 2); + _path.lineTo(size.width, size.height / 2 - size.height * _value * 0.2); + canvas.drawPath(_path, _paint); + } + + @override + bool shouldRepaint(WavePainter oldDelegate) { + return oldDelegate._fraction != _fraction; + } +} + +class WaveLoader extends StatefulWidget { + final Color color; + WaveLoader({this.color, Key key}) : super(key: key); + @override + _WaveLoaderState createState() => _WaveLoaderState(); +} + +class _WaveLoaderState extends State + with SingleTickerProviderStateMixin { + double _fraction = 0.0; + Animation animation; + AnimationController _controller; + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, duration: Duration(milliseconds: 1000)); + animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _fraction = animation.value; + }); + }); + _controller.forward(); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reset(); + } else if (status == AnimationStatus.dismissed) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: WavePainter(_fraction, widget.color ?? Colors.white)); + } +} + +//Love shape +class LovePainter extends CustomPainter { + @override + void paint(Canvas canvas, Size size) { + Path _path = Path(); + Paint _paint = Paint() + ..color = Colors.red + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round; + + _path.moveTo(size.width / 2, size.height / 6); + _path.quadraticBezierTo(size.width / 4, 0, size.width / 8, size.height / 6); + _path.quadraticBezierTo( + 0, size.height / 3, size.width / 8, size.height * 0.55); + _path.quadraticBezierTo( + size.width / 4, size.height * 0.8, size.width / 2, size.height); + _path.quadraticBezierTo(size.width * 0.75, size.height * 0.8, + size.width * 7 / 8, size.height * 0.55); + _path.quadraticBezierTo( + size.width, size.height / 3, size.width * 7 / 8, size.height / 6); + _path.quadraticBezierTo( + size.width * 3 / 4, 0, size.width / 2, size.height / 6); + + canvas.drawPath(_path, _paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} + +//Line buffer indicator +//Not used +class LinePainter extends CustomPainter { + double _fraction; + Paint _paint; + Color _maincolor; + LinePainter(this._fraction, this._maincolor) { + _paint = Paint() + ..color = _maincolor + ..strokeWidth = 2.0 + ..strokeCap = StrokeCap.round; + } + + @override + void paint(Canvas canvas, Size size) { + canvas.drawLine(Offset(0, size.height / 2.0), + Offset(size.width * _fraction, size.height / 2.0), _paint); + } + + @override + bool shouldRepaint(LinePainter oldDelegate) { + return oldDelegate._fraction != _fraction; + } +} + +class LineLoader extends StatefulWidget { + @override + _LineLoaderState createState() => _LineLoaderState(); +} + +class _LineLoaderState extends State + with SingleTickerProviderStateMixin { + double _fraction = 0.0; + Animation animation; + AnimationController controller; + @override + void initState() { + super.initState(); + controller = + AnimationController(vsync: this, duration: Duration(milliseconds: 500)); + animation = Tween(begin: 0.0, end: 1.0).animate(controller) + ..addListener(() { + if (mounted) + setState(() { + _fraction = animation.value; + }); + }); + controller.forward(); + controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + controller.reset(); + } else if (status == AnimationStatus.dismissed) { + controller.forward(); + } + }); + } + + @override + void dispose() { + controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: LinePainter(_fraction, Theme.of(context).accentColor)); + } +} + +class ImageRotate extends StatefulWidget { + final String title; + final String path; + ImageRotate({this.title, this.path, Key key}) : super(key: key); + @override + _ImageRotateState createState() => _ImageRotateState(); +} + +class _ImageRotateState extends State + with SingleTickerProviderStateMixin { + Animation _animation; + AnimationController _controller; + double _value; + @override + void initState() { + super.initState(); + _value = 0; + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 2000), + ); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _value = _animation.value; + }); + }); + _controller.forward(); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reset(); + } else if (status == AnimationStatus.dismissed) { + _controller.forward(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Transform.rotate( + angle: 2 * math.pi * _value, + child: Container( + padding: EdgeInsets.all(10.0), + child: ClipRRect( + borderRadius: BorderRadius.all(Radius.circular(15.0)), + child: Container( + height: 30.0, + width: 30.0, + color: Colors.white, + child: Image.file(File("${widget.path}")), + ), + ), + ), + ); + } +} + +class LoveOpen extends StatefulWidget { + @override + _LoveOpenState createState() => _LoveOpenState(); +} + +class _LoveOpenState extends State + with SingleTickerProviderStateMixin { + Animation _animationA; + AnimationController _controller; + + @override + void initState() { + super.initState(); + _controller = AnimationController( + vsync: this, + duration: Duration(milliseconds: 300), + ); + + _animationA = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) setState(() {}); + }); + + _controller.forward(); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.reset(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + Widget _littleHeart(double scale, double value, double angle) => Container( + alignment: Alignment.centerLeft, + padding: EdgeInsets.only(left: value), + child: ScaleTransition( + scale: _animationA, + alignment: Alignment.center, + child: Transform.rotate( + angle: angle, + child: SizedBox( + height: 5 * scale, + width: 6 * scale, + child: CustomPaint( + painter: LovePainter(), + ), + ), + ), + ), + ); + + @override + Widget build(BuildContext context) { + return Container( + width: 50, + height: 50, + alignment: Alignment.center, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Row( + children: [ + _littleHeart(0.5, 10, -math.pi / 6), + _littleHeart(1.2, 3, 0), + ], + ), + Row( + children: [ + _littleHeart(0.8, 6, math.pi * 1.5), + _littleHeart(0.9, 24, math.pi / 2), + ], + ), + Row( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + _littleHeart(1, 8, -math.pi * 0.7), + _littleHeart(0.8, 8, math.pi), + _littleHeart(0.6, 3, -math.pi * 1.2) + ], + ), + ], + ), + ); + } +} diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index fc0510b..ef463f8 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -13,6 +13,11 @@ import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/episodebrief.dart'; import 'package:tsacdop/episodes/episodedetail.dart'; import 'package:tsacdop/util/colorize.dart'; +import 'package:tsacdop/util/context_extension.dart'; +import 'package:tsacdop/util/custompaint.dart'; +import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; + +enum Layout { two, three } class EpisodeGrid extends StatelessWidget { final List episodes; @@ -20,15 +25,33 @@ class EpisodeGrid extends StatelessWidget { final bool showDownload; final bool showNumber; final int episodeCount; - EpisodeGrid( - {Key key, - @required this.episodes, - this.showDownload = false, - this.showFavorite = false, - this.showNumber = false, - this.episodeCount = 0 - }) - : super(key: key); + final Layout layout; + final bool reverse; + Future _isListened(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + return await dbHelper.isListened(episode.enclosureUrl); + } + + Future _isLiked(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + return await dbHelper.isLiked(episode.enclosureUrl); + } + + String _stringForSeconds(double seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } + + EpisodeGrid({ + Key key, + @required this.episodes, + this.showDownload = false, + this.showFavorite = false, + this.showNumber = false, + this.episodeCount = 0, + this.layout = Layout.three, + this.reverse, + }) : super(key: key); @override Widget build(BuildContext context) { @@ -100,12 +123,12 @@ class EpisodeGrid extends StatelessWidget { } return SliverPadding( - padding: - const EdgeInsets.only(top: 10.0, bottom: 5.0, left: 15.0, right: 15.0), + padding: const EdgeInsets.only( + top: 10.0, bottom: 5.0, left: 15.0, right: 15.0), sliver: SliverGrid( gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( - childAspectRatio: 1, - crossAxisCount: 3, + childAspectRatio: layout == Layout.three ? 1 : 1.5, + crossAxisCount: layout == Layout.three ? 3 : 2, mainAxisSpacing: 6.0, crossAxisSpacing: 6.0, ), @@ -120,147 +143,291 @@ class EpisodeGrid extends StatelessWidget { audio.queue.playlist.map((e) => e.enclosureUrl).toList()), builder: (_, data, __) => OpenContainerWrapper( episode: episodes[index], - closedBuilder: (context, action, boo) => 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, - episodes[index], - context, - data.item1 == episodes[index], - data.item2.contains(episodes[index].enclosureUrl)), - onTap: action, - child: Container( - padding: const EdgeInsets.all(8.0), + closedBuilder: (context, action, boo) => FutureBuilder( + future: _isListened(episodes[index]), + initialData: 0, + builder: (BuildContext context, AsyncSnapshot snapshot) { + return Container( decoration: BoxDecoration( - 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: [ - Expanded( - flex: 2, - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Container( - height: _width / 16, - width: _width / 16, - child: boo - ? Center() - : CircleAvatar( - backgroundColor: - _c.withOpacity(0.5), - backgroundImage: FileImage(File( - "${episodes[index].imagePath}")), - ), - ), - Spacer(), - episodes[index].isNew == 1 - ? Text('New', - style: TextStyle( - color: Colors.red, - fontStyle: FontStyle.italic)) - : Center(), - Padding( - padding: - EdgeInsets.symmetric(horizontal: 2), - ), - showNumber - ? Container( - alignment: Alignment.topRight, - child: Text( - (episodeCount - index).toString(), - style: GoogleFonts.teko( - textStyle: TextStyle( - fontSize: _width / 24, - color: _c, - ), - ), - ), - ) - : Center(), - ], + borderRadius: + BorderRadius.all(Radius.circular(5.0)), + color: snapshot.data > 0 + ? context.brightness == Brightness.light + ? context.primaryColor + : Color.fromRGBO(40, 40, 40, 1) + : context.scaffoldBackgroundColor, + boxShadow: [ + BoxShadow( + color: context.brightness == Brightness.light + ? context.primaryColor + : Color.fromRGBO(40, 40, 40, 1), + blurRadius: 0.5, + spreadRadius: 0.5, ), - ), - Expanded( - flex: 5, - child: Container( - alignment: Alignment.topLeft, - padding: EdgeInsets.only(top: 2.0), - child: Text( - episodes[index].title, - style: TextStyle( - // fontSize: _width / 32, - ), - maxLines: 4, - overflow: TextOverflow.fade, + ]), + 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, + episodes[index], + context, + data.item1 == episodes[index], + data.item2 + .contains(episodes[index].enclosureUrl)), + onTap: action, + child: Container( + padding: const EdgeInsets.all(8.0), + decoration: BoxDecoration( + borderRadius: + BorderRadius.all(Radius.circular(5.0)), + border: Border.all( + color: context.brightness == Brightness.light + ? context.primaryColor + : context.scaffoldBackgroundColor, + width: 1.0, ), ), - ), - Expanded( - flex: 1, - child: Row( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, children: [ - Align( - alignment: Alignment.bottomLeft, - child: Text( - episodes[index].dateToString(), - style: TextStyle( - fontSize: _width / 35, - color: _c, - fontStyle: FontStyle.italic), - ), - ), - Spacer(), - Padding( - padding: EdgeInsets.all(1), - ), - showFavorite - ? Container( - alignment: Alignment.bottomRight, - child: (episodes[index].liked == 0) + Expanded( + flex: 2, + child: Row( + mainAxisAlignment: + MainAxisAlignment.start, + children: [ + Container( + height: _width / 16, + width: _width / 16, + child: boo ? Center() - : IconTheme( - data: IconThemeData(size: 15), - child: Icon( - Icons.favorite, - color: Colors.red, + : CircleAvatar( + backgroundColor: + _c.withOpacity(0.5), + backgroundImage: FileImage(File( + "${episodes[index].imagePath}")), + ), + ), + Spacer(), + episodes[index].isNew == 1 + ? Container( + padding: EdgeInsets.symmetric( + horizontal: 2), + child: Text('New', + style: TextStyle( + color: Colors.red, + fontStyle: + FontStyle.italic)), + ) + : Center(), + Selector( + selector: (_, audio) => + audio.episode, + builder: (_, data, __) { + return (episodes[index] + .enclosureUrl == + data?.enclosureUrl) + ? Container( + height: 20, + width: 20, + margin: + EdgeInsets.symmetric( + horizontal: 2), + decoration: BoxDecoration( + color: Colors.white, + shape: BoxShape.circle, + ), + child: SizedBox( + height: 8, + width: 15, + child: WaveLoader( + color: context + .accentColor))) + : layout == Layout.two && + snapshot.data > 0 + ? Container( + height: 20, + width: 20, + margin: EdgeInsets + .symmetric( + horizontal: + 2), + decoration: + BoxDecoration( + color: context + .accentColor, + shape: + BoxShape.circle, + ), + child: SizedBox( + height: 10, + width: 15, + child: CustomPaint( + painter: + ListenedPainter( + Colors.white, + )), + )) + : Center(); + }), + showDownload || layout == Layout.two + ? Container( + child: (episodes[index] + .enclosureUrl != + episodes[index].mediaId) + ? Container( + height: 20, + width: 20, + margin: EdgeInsets + .symmetric( + horizontal: 5), + padding: EdgeInsets + .symmetric( + horizontal: 2), + decoration: + BoxDecoration( + color: context + .accentColor, + shape: + BoxShape.circle, + ), + child: Icon( + Icons.done_all, + size: 15, + color: Colors.white, + ), + ) + : Center(), + ) + : Center(), + showNumber + ? Container( + alignment: Alignment.topRight, + child: Text( + reverse + ? (index + 1).toString() + : (episodeCount - index) + .toString(), + style: GoogleFonts.teko( + textStyle: TextStyle( + fontSize: _width / 24, + color: _c, + ), ), ), - ) - : Center(), + ) + : Center(), + ], + ), + ), + Expanded( + flex: 5, + child: Container( + alignment: Alignment.topLeft, + padding: EdgeInsets.only(top: 2.0), + child: Text( + episodes[index].title, + style: TextStyle( + // fontSize: _width / 32, + ), + maxLines: 4, + overflow: TextOverflow.fade, + ), + ), + ), + Expanded( + flex: 1, + child: Row( + children: [ + Align( + alignment: Alignment.bottomLeft, + child: Text( + episodes[index].dateToString(), + style: TextStyle( + fontSize: _width / 35, + color: _c, + fontStyle: FontStyle.italic), + ), + ), + Spacer(), + layout == Layout.two && + episodes[index].duration != 0 + ? Container( + alignment: Alignment.center, + child: Text( + _stringForSeconds( + episodes[index] + .duration + .toDouble()) + + '|', + style: TextStyle( + fontSize: _width / 35), + ), + ) + : Center(), + layout == Layout.two && + episodes[index] + .enclosureLength != + null && + episodes[index] + .enclosureLength != + 0 + ? Container( + alignment: Alignment.center, + child: Text( + ((episodes[index] + .enclosureLength) ~/ + 1000000) + .toString() + + 'MB', + style: TextStyle( + fontSize: _width / 35), + ), + ) + : Center(), + Padding( + padding: EdgeInsets.all(1), + ), + showFavorite || layout == Layout.two + ? FutureBuilder( + future: + _isLiked(episodes[index]), + initialData: false, + builder: (context, snapshot) => + Container( + alignment: + Alignment.bottomRight, + child: (snapshot.data) + ? IconTheme( + data: IconThemeData( + size: 15), + child: Icon( + Icons.favorite, + color: Colors.red, + ), + ) + : Center(), + ), + ) + : Center(), + ], + ), + ), ], ), ), - ], + ), ), - ), - ), - ), - ), + ); + }), ), ); }, @@ -271,7 +438,6 @@ class EpisodeGrid extends StatelessWidget { } } - class OpenContainerWrapper extends StatelessWidget { const OpenContainerWrapper({ this.closedBuilder, @@ -318,5 +484,3 @@ class OpenContainerWrapper extends StatelessWidget { ); } } - - diff --git a/pubspec.yaml b/pubspec.yaml index 57d7205..ed6d71a 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -64,6 +64,7 @@ dev_dependencies: extended_nested_scroll_view: ^0.4.0 connectivity: ^0.4.8+2 flare_flutter: ^2.0.1 + rxdart: ^0.23.1 # For information on the generic Dart part of this file, see the