diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index 3287bb6..0000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Flutter", - "request": "launch", - "type": "dart" - } - ] -} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json deleted file mode 100644 index e0f15db..0000000 --- a/.vscode/settings.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "java.configuration.updateBuildConfiguration": "automatic" -} \ No newline at end of file diff --git a/android/app/src/main/res/drawable/launch_background.xml b/android/app/src/main/res/drawable/launch_background.xml index 5bd371a..82de3f0 100644 --- a/android/app/src/main/res/drawable/launch_background.xml +++ b/android/app/src/main/res/drawable/launch_background.xml @@ -14,4 +14,6 @@ android:gravity="bottom" android:src="@mipmap/text" /> + diff --git a/android/app/src/main/res/drawable/launch_background_night.xml b/android/app/src/main/res/drawable/launch_background_night.xml index 18cc931..0ac4190 100644 --- a/android/app/src/main/res/drawable/launch_background_night.xml +++ b/android/app/src/main/res/drawable/launch_background_night.xml @@ -1,7 +1,7 @@ - + + android:src="@mipmap/text_light" /> \ No newline at end of file diff --git a/android/app/src/main/res/drawable/normal_background.xml b/android/app/src/main/res/drawable/normal_background.xml deleted file mode 100644 index 689ff5b..0000000 --- a/android/app/src/main/res/drawable/normal_background.xml +++ /dev/null @@ -1,17 +0,0 @@ - - - - - - - - - - - - \ No newline at end of file diff --git a/android/app/src/main/res/values/styles.xml b/android/app/src/main/res/values/styles.xml index 25442f8..f9b1bb2 100644 --- a/android/app/src/main/res/values/styles.xml +++ b/android/app/src/main/res/values/styles.xml @@ -5,7 +5,4 @@ Flutter draws its first frame --> @drawable/launch_background - diff --git a/lib/class/audiostate.dart b/lib/class/audiostate.dart index b720f35..da0cc46 100644 --- a/lib/class/audiostate.dart +++ b/lib/class/audiostate.dart @@ -11,7 +11,7 @@ import 'package:audiofileplayer/audio_system.dart'; import 'package:tsacdop/local_storage/key_value_storage.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; -enum AudioState { load, play, pause, complete, error, stop } +//enum AudioState { load, play, pause, complete, error, stop } class PlayHistory { DBHelper dbHelper = DBHelper(); @@ -90,6 +90,14 @@ class AudioPlayer extends ChangeNotifier { bool _remoteAudioLoading = false; String _remoteErrorMessage; double _seekSliderValue = 0.0; + int _lastPostion; + bool _skip = false; + bool _stopOnComplete = false; + Timer _stopTimer; + //Show stopwatch after user setting timer. + bool _showStopWatch = false; + + final Logger _logger = Logger('audiofileplayer'); bool get backgroundAudioPlaying => _backgroundAudioPlaying; @@ -99,16 +107,20 @@ class AudioPlayer extends ChangeNotifier { double get seekSliderValue => _seekSliderValue; String get remoteErrorMessage => _remoteErrorMessage; bool get playerRunning => _playerRunning; - int _lastPostion; int get lastPositin => _lastPostion; Playlist get queue => _queue; - EpisodeBrief get episode => _episode; + bool get stopOnComplete => _stopOnComplete; + bool get showStopWatch => _showStopWatch; + + + set setStopOnComplete(bool boo) { + _stopOnComplete = boo; + } loadPlaylist() async { await _queue.getPlaylist(); _lastPostion = await storage.getInt(); - print(_lastPostion); } episodeLoad(EpisodeBrief episode) async { @@ -134,22 +146,27 @@ class AudioPlayer extends ChangeNotifier { _backgroundAudioPlaying = false; await _queue.getPlaylist(); _episode = _queue.playlist.first; + _skip = true; await _play(_episode); _playerRunning = true; notifyListeners(); } playNext() async { + storage.saveInt(0); + _lastPostion = 0; PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, backgroundAudioDuration, seekSliderValue); await dbHelper.saveHistory(history); await _queue.delFromPlaylist(_episode); - if (_queue.playlist.length > 0) { + if (_queue.playlist.length > 0 && !_stopOnComplete) { playlistLoad(); } else { + _stopOnComplete = false; _backgroundAudioPlaying = false; _remoteAudioLoading = false; _playerRunning = false; + _disposeAudio(); notifyListeners(); } } @@ -170,7 +187,7 @@ class AudioPlayer extends ChangeNotifier { _pauseBackgroundAudio(); notifyListeners(); PlayHistory history = PlayHistory(_episode.title, _episode.enclosureUrl, - backgroundAudioDuration, seekSliderValue); + backgroundAudioPosition, seekSliderValue); await dbHelper.saveHistory(history); } @@ -191,16 +208,42 @@ class AudioPlayer extends ChangeNotifier { _backgroundAudio.seek(positionSeconds); AudioSystem.instance.setPlaybackState(true, positionSeconds); } - + //Set sleep time + sleepTimer(int mins) { + _showStopWatch = true; + notifyListeners(); + _stopTimer = Timer(Duration(minutes: mins),(){ + _stopOnComplete = false; + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + _playerRunning = false; + _showStopWatch = false; + _disposeAudio(); + notifyListeners(); + }); + } +//Cancel sleep timer + cancelTimer(){ + _stopTimer.cancel(); + _showStopWatch = false; + notifyListeners(); + } + + _disposeAudio() { + pauseAduio(); + AudioSystem.instance?.stopBackgroundDisplay(); + AudioSystem.instance?.removeMediaEventListener(_mediaEventListener); + _backgroundAudio?.dispose(); + } + @override dispose() { - pauseAduio(); - AudioSystem.instance.removeMediaEventListener(_mediaEventListener); - _backgroundAudio?.dispose(); + _disposeAudio(); super.dispose(); } _play(EpisodeBrief episodeBrief) async { + AudioSystem.instance.addMediaEventListener(_mediaEventListener); String url = _queue.playlist.first.enclosureUrl; _getFile(url).then((result) { result == 'NotDownload' @@ -225,6 +268,42 @@ class AudioPlayer extends ChangeNotifier { return ByteData.view(audio.buffer); } + onDuration(double durationSeconds) { + _backgroundAudioDurationSeconds = durationSeconds; + _remoteAudioLoading = false; + _backgroundAudioPlaying = true; + if (_skip) { + _forwardBackgroundAudio(_lastPostion.toDouble()); + _backgroundAudioPositionSeconds = _lastPostion.toDouble(); + } + _skip = false; + _setNotification(true); + notifyListeners(); + } + + onPosition(double positionSeconds) { + if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) { + _seekSliderValue = + _backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds; + _backgroundAudioPositionSeconds = positionSeconds; + notifyListeners(); + } else { + _seekSliderValue = 1; + _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; + notifyListeners(); + } + _lastPostion = positionSeconds.toInt(); + storage.saveInt(_lastPostion); + } + + onError(String message) { + _remoteErrorMessage = message; + _backgroundAudio.dispose(); + _backgroundAudio = null; + _backgroundAudioPlaying = false; + _remoteAudioLoading = false; + } + void _initbackgroundAudioPlayerLocal(String path) { _remoteErrorMessage = null; _remoteAudioLoading = true; @@ -232,35 +311,14 @@ class AudioPlayer extends ChangeNotifier { _backgroundAudio?.pause(); _backgroundAudioPositionSeconds = 0; _setNotification(false); - _backgroundAudio = - Audio.loadFromByteData(audio, onDuration: (double durationSeconds) { - _backgroundAudioDurationSeconds = durationSeconds; - _remoteAudioLoading = false; - _backgroundAudioPlaying = true; - _setNotification(true); - notifyListeners(); - }, onPosition: (double positionSeconds) { - if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) { - _seekSliderValue = - _backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds; - _backgroundAudioPositionSeconds = positionSeconds; - notifyListeners(); - } else { - _seekSliderValue = 1; - _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; - notifyListeners(); - } - storage.saveInt(positionSeconds.toInt()); - }, onError: (String message) { - _remoteErrorMessage = message; - _backgroundAudio.dispose(); - _backgroundAudio = null; - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - }, onComplete: () { - playNext(); - }, looping: false, playInBackground: true) - ..play(); + _backgroundAudio = Audio.loadFromByteData(audio, + onDuration: (double durationSeconds) => onDuration(durationSeconds), + onPosition: (double positionSeconds) => onPosition(positionSeconds), + onError: (String message) => onError(message), + onComplete: () => playNext(), + looping: false, + playInBackground: true) + ..play(); } void _initbackgroundAudioPlayer(String url) { @@ -270,35 +328,14 @@ class AudioPlayer extends ChangeNotifier { _backgroundAudio?.pause(); _backgroundAudioPositionSeconds = 0; _setNotification(false); - _backgroundAudio = - Audio.loadFromRemoteUrl(url, onDuration: (double durationSeconds) { - _backgroundAudioDurationSeconds = durationSeconds; - _remoteAudioLoading = false; - _backgroundAudioPlaying = true; - _setNotification(true); - notifyListeners(); - }, onPosition: (double positionSeconds) { - if (_backgroundAudioPositionSeconds < _backgroundAudioDurationSeconds) { - _seekSliderValue = - _backgroundAudioPositionSeconds / _backgroundAudioDurationSeconds; - _backgroundAudioPositionSeconds = positionSeconds; - notifyListeners(); - } else { - _seekSliderValue = 1; - _backgroundAudioPositionSeconds = _backgroundAudioDurationSeconds; - notifyListeners(); - } - storage.saveInt(positionSeconds.toInt()); - }, onError: (String message) { - _remoteErrorMessage = message; - _backgroundAudio.dispose(); - _backgroundAudio = null; - _backgroundAudioPlaying = false; - _remoteAudioLoading = false; - }, onComplete: () { - playNext(); - }, looping: false, playInBackground: true) - ..resume(); + _backgroundAudio = Audio.loadFromRemoteUrl(url, + onDuration: (double durationSeconds) => onDuration(durationSeconds), + onPosition: (double positionSeconds) => onPosition(positionSeconds), + onError: (String message) => onError(message), + onComplete: () => playNext(), + looping: false, + playInBackground: true) + ..resume(); } void _mediaEventListener(MediaEvent mediaEvent) { @@ -341,7 +378,7 @@ class AudioPlayer extends ChangeNotifier { } } - Future _setNotification(bool b) async { + Future _setNotification(bool boo) async { final Uint8List imageBytes = File('${_episode.imagePath}').readAsBytesSync(); AudioSystem.instance.setMetadata(AudioMetadata( @@ -351,7 +388,7 @@ class AudioPlayer extends ChangeNotifier { genre: "Podcast", durationSeconds: _backgroundAudioDurationSeconds, artBytes: imageBytes)); - AudioSystem.instance.setPlaybackState(b, _backgroundAudioPositionSeconds); + AudioSystem.instance.setPlaybackState(boo, _backgroundAudioPositionSeconds); AudioSystem.instance.setAndroidNotificationButtons([ AndroidMediaButtonType.pause, _forwardButton, diff --git a/lib/class/importompl.dart b/lib/class/importompl.dart index e35ee13..ad05b61 100644 --- a/lib/class/importompl.dart +++ b/lib/class/importompl.dart @@ -10,6 +10,7 @@ class ImportOmpl extends ChangeNotifier{ set rssTitle(String title){ _rssTitle = title; + notifyListeners(); } ImportState get importState => _importState; diff --git a/lib/class/podcast_group.dart b/lib/class/podcast_group.dart index 85df4d7..711cca8 100644 --- a/lib/class/podcast_group.dart +++ b/lib/class/podcast_group.dart @@ -45,6 +45,15 @@ class PodcastGroup { } } + Color getColor() { + if (color != '#000000') { + int colorInt = int.parse('FF' + color.toUpperCase(), radix: 16); + return Color(colorInt).withOpacity(1.0); + } else { + return Colors.blue[400]; + } + } + List _podcasts; List get podcasts => _podcasts; @@ -102,15 +111,25 @@ class GroupList extends ChangeNotifier { } Future delGroup(PodcastGroup podcastGroup) async { - _groups.remove(podcastGroup); + _isLoading = true; notifyListeners(); + podcastGroup.podcastList.forEach((podcast) { + if (!_groups.first.podcastList.contains(podcast)) { + _groups[0].podcastList.insert(0, podcast); + } + }); _saveGroup(); + _groups.remove(podcastGroup); + await _groups[0].getPodcasts(); + _isLoading = false; + notifyListeners(); } - void updateGroup(PodcastGroup podcastGroup) { + updateGroup(PodcastGroup podcastGroup) async{ var oldGroup = _groups.firstWhere((it) => it.id == podcastGroup.id); var index = _groups.indexOf(oldGroup); _groups.replaceRange(index, index + 1, [podcastGroup]); + await podcastGroup.getPodcasts(); notifyListeners(); _saveGroup(); } @@ -137,6 +156,7 @@ class GroupList extends ChangeNotifier { return result; } + //Change podcast groups changeGroup(String id, List list) async { _isLoading = true; notifyListeners(); @@ -154,15 +174,16 @@ class GroupList extends ChangeNotifier { notifyListeners(); } + //Unsubscribe podcast removePodcast(String id) async { _isLoading = true; notifyListeners(); _groups.forEach((group) async { - group.podcastList.remove(id); + group.podcastList.remove(id); }); _saveGroup(); await dbHelper.delPodcastLocal(id); - await Future.forEach(_groups, (group) async { + await Future.forEach(_groups, (group) async { await group.getPodcasts(); }); _isLoading = false; diff --git a/lib/home/appbar/about.dart b/lib/home/appbar/about.dart index 4b6df86..45b511f 100644 --- a/lib/home/appbar/about.dart +++ b/lib/home/appbar/about.dart @@ -29,7 +29,7 @@ class AboutApp extends StatelessWidget { mainAxisAlignment: MainAxisAlignment.center, mainAxisSize: MainAxisSize.min, children: [ - Icon(icons, color: Colors.grey[700]), + Icon(icons, color: Theme.of(context).accentColor), Padding( padding: EdgeInsets.symmetric(horizontal: 10), ), diff --git a/lib/home/appbar/addpodcast.dart b/lib/home/appbar/addpodcast.dart index 974b2f6..6319f67 100644 --- a/lib/home/appbar/addpodcast.dart +++ b/lib/home/appbar/addpodcast.dart @@ -10,10 +10,8 @@ import 'package:provider/provider.dart'; import 'package:path_provider/path_provider.dart'; import 'package:image/image.dart' as img; import 'package:fluttertoast/fluttertoast.dart'; -import 'package:tsacdop/class/audiostate.dart'; import 'package:tsacdop/class/fireside_data.dart'; import 'package:uuid/uuid.dart'; -import 'package:tuple/tuple.dart'; import 'package:tsacdop/class/importompl.dart'; import 'package:tsacdop/class/podcast_group.dart'; @@ -32,30 +30,9 @@ class MyHomePage extends StatefulWidget { class _MyHomePageState extends State { final _MyHomePageDelegate _delegate = _MyHomePageDelegate(); final GlobalKey _scaffoldKey = GlobalKey(); - bool _loadPlay; - - static String _stringForSeconds(int seconds) { - if (seconds == null) return null; - return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; - } - - @override - void initState() { - super.initState(); - _loadPlay = false; - _getPlaylist(); - } - - _getPlaylist() async { - await Provider.of(context, listen: false).loadPlaylist(); - setState(() { - _loadPlay = true; - }); - } - + @override Widget build(BuildContext context) { - var audio = Provider.of(context, listen: false); return AnnotatedRegion( value: SystemUiOverlayStyle( statusBarIconBrightness: Theme.of(context).accentColorBrightness, @@ -78,44 +55,14 @@ class _MyHomePageState extends State { }, ), title: Image( - image: AssetImage('assets/text.png'), + image: Theme.of(context).brightness == Brightness.light + ? AssetImage('assets/text.png') : AssetImage('assets/text_light.png'), height: 30, ), actions: [ PopupMenu(), ], ), - floatingActionButton: Selector>( - selector: (_, audio) => Tuple3(audio.playerRunning, audio.queue, audio.lastPositin), - builder: (_, data, __) => !_loadPlay - ? Center() - : data.item1 || data.item2.playlist.length == 0 - ? Center() - : FloatingActionButton.extended( - tooltip: 'Play from playlist', - highlightElevation: 2, - hoverElevation: 2, - onPressed: () => audio.playlistLoad(), - elevation: 1, - backgroundColor: Theme.of(context).accentColor, - label: Text(_stringForSeconds(data.item3), style: TextStyle(color: Colors.white)), - icon: Stack( - alignment: Alignment.center, - children: [ - CircleAvatar( - radius: 15, - backgroundImage: FileImage(File( - "${data.item2.playlist.first.imagePath}")), - ), - Container( - height: 30.0, - decoration: BoxDecoration( - shape: BoxShape.circle, - color: Colors.black38), - ), - ], - )), - ), body: Home(), ), ), @@ -163,7 +110,8 @@ class _MyHomePageDelegate extends SearchDelegate { child: Container( padding: EdgeInsets.only(top: 400), child: Image( - image: AssetImage('assets/listennotes.png'), + image: Theme.of(context).brightness == Brightness.light + ? AssetImage('assets/listennotes.png') : AssetImage('assets/listennotes_light.png'), height: 20, ), )); diff --git a/lib/home/appbar/popupmenu.dart b/lib/home/appbar/popupmenu.dart index d7cf4d5..0953f57 100644 --- a/lib/home/appbar/popupmenu.dart +++ b/lib/home/appbar/popupmenu.dart @@ -49,6 +49,7 @@ class PopupMenu extends StatelessWidget { Widget build(BuildContext context) { ImportOmpl importOmpl = Provider.of(context, listen: false); GroupList groupList = Provider.of(context, listen: false); + _refreshAll() async { var dbHelper = DBHelper(); List podcastList = await dbHelper.getPodcastLocalAll(); diff --git a/lib/home/audioplayer.dart b/lib/home/audioplayer.dart index d4fdd7d..99d6108 100644 --- a/lib/home/audioplayer.dart +++ b/lib/home/audioplayer.dart @@ -1,3 +1,4 @@ +import 'dart:async'; import 'dart:io'; import 'dart:math' as math; @@ -25,12 +26,118 @@ class _PlayerWidgetState extends State { return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; } + List minsToSelect = [1, 5, 10, 15, 20, 25, 30, 45, 60, 70, 80, 90, 99]; + //Show playlist widget. bool _showlist; + //Show timer choose widget. + bool _showTimer; + //Store selected timer mins. + int _minSelected; + //Left time after user setting timer. + int _timeLeft; + Timer _timer; @override void initState() { super.initState(); _showlist = false; + _showTimer = false; + _minSelected = 5; + _timeLeft = 0; + } + + setTimer() { + _timeLeft = _minSelected; + _timer = Timer.periodic(Duration(minutes: 1), (timer) { + setState(() { + if(_timeLeft < 1){ + _timer.cancel(); + } else{ + _timeLeft = _timeLeft - 1; + } + }); + }); + } + + Widget _sleepTimer(BuildContext context) { + var audio = Provider.of(context); + return Container( + height: 50, + margin: EdgeInsets.all(10.0), + padding: EdgeInsets.symmetric(horizontal: 10.0), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + mainAxisSize: MainAxisSize.min, + children: [ + Expanded( + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + crossAxisAlignment: CrossAxisAlignment.center, + children: minsToSelect + .map((e) => InkWell( + onTap: () => setState(() => _minSelected = e), + child: AnimatedContainer( + duration: Duration(milliseconds: 300), + curve: Curves.elasticOut, + margin: EdgeInsets.symmetric(horizontal: 10.0), + decoration: BoxDecoration( + color: (e == _minSelected) + ? Theme.of(context).accentColor + : Colors.grey[400], + shape: BoxShape.circle, + ), + alignment: Alignment.center, + height: (e == _minSelected) ? 40 : 30, + width: (e == _minSelected) ? 40 : 30, + child: Text(e.toString(), + style: TextStyle( + color: (e == _minSelected) + ? Colors.white + : Colors.black)), + ), + )) + .toList(), + ), + ), + ), + Container( + width: 100, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Material( + color: Colors.transparent, + child: IconButton( + icon: Icon(Icons.clear), + onPressed: () { + setState(() => _showTimer = false); + }, + ), + ), + Material( + color: Colors.transparent, + child: IconButton( + icon: Icon(Icons.done), + onPressed: () { + setState(() { + _showTimer = false; + }); + audio.sleepTimer(_minSelected); + setTimer(); + }, + ), + ), + ], + ), + ), + ], + ), + ); } Widget _expandedPanel(BuildContext context) { @@ -104,13 +211,16 @@ class _PlayerWidgetState extends State { padding: EdgeInsets.only(left: 30, right: 30), child: SliderTheme( data: SliderTheme.of(context).copyWith( - activeTrackColor: Colors.blue[100], + activeTrackColor: Theme.of(context) + .accentColor + .withOpacity(0.5), inactiveTrackColor: Colors.grey[300], trackHeight: 3.0, - thumbColor: Colors.blue[400], + thumbColor: Theme.of(context).accentColor, thumbShape: RoundSliderThumbShape( enabledThumbRadius: 6.0), - overlayColor: Colors.blue.withAlpha(32), + overlayColor: + Theme.of(context).accentColor.withAlpha(32), overlayShape: RoundSliderOverlayShape(overlayRadius: 14.0), ), @@ -167,113 +277,192 @@ class _PlayerWidgetState extends State { child: Selector( selector: (_, audio) => audio.backgroundAudioPlaying, builder: (_, backplay, __) { - return Row( - mainAxisSize: MainAxisSize.min, - mainAxisAlignment: MainAxisAlignment.center, - children: [ - IconButton( - padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? () => audio.forwardAudio(-10) - : null, - iconSize: 32.0, - icon: Icon(Icons.replay_10), - color: Theme.of(context).tabBarTheme.labelColor), - backplay - ? IconButton( - padding: - EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? () { - audio.pauseAduio(); - } - : null, - iconSize: 40.0, - icon: Icon(Icons.pause_circle_filled), - color: - Theme.of(context).tabBarTheme.labelColor) - : IconButton( - padding: - EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? null - : () { - audio.resumeAudio(); - }, - iconSize: 40.0, - icon: Icon(Icons.play_circle_filled), - color: - Theme.of(context).tabBarTheme.labelColor), - IconButton( - padding: EdgeInsets.symmetric(horizontal: 30.0), - onPressed: backplay - ? () => audio.forwardAudio(30) - : null, - iconSize: 32.0, - icon: Icon(Icons.forward_30), - color: Theme.of(context).tabBarTheme.labelColor), - ], + return Material( + color: Colors.transparent, + child: Row( + mainAxisSize: MainAxisSize.min, + mainAxisAlignment: MainAxisAlignment.center, + children: [ + IconButton( + padding: EdgeInsets.symmetric(horizontal: 30.0), + onPressed: backplay + ? () => audio.forwardAudio(-10) + : null, + iconSize: 32.0, + icon: Icon(Icons.replay_10), + color: + Theme.of(context).tabBarTheme.labelColor), + backplay + ? IconButton( + padding: + EdgeInsets.symmetric(horizontal: 30.0), + onPressed: backplay + ? () { + audio.pauseAduio(); + } + : null, + iconSize: 40.0, + icon: Icon(Icons.pause_circle_filled), + color: Theme.of(context) + .tabBarTheme + .labelColor) + : IconButton( + padding: + EdgeInsets.symmetric(horizontal: 30.0), + onPressed: backplay + ? null + : () { + audio.resumeAudio(); + }, + iconSize: 40.0, + icon: Icon(Icons.play_circle_filled), + color: Theme.of(context) + .tabBarTheme + .labelColor), + IconButton( + padding: EdgeInsets.symmetric(horizontal: 30.0), + onPressed: backplay + ? () => audio.forwardAudio(30) + : null, + iconSize: 32.0, + icon: Icon(Icons.forward_30), + color: + Theme.of(context).tabBarTheme.labelColor), + ], + ), ); }, ), ), Spacer(), - Container( - height: 50.0, - margin: EdgeInsets.all(10.0), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - borderRadius: BorderRadius.all(Radius.circular(10.0)), - ), - child: Selector( - selector: (_, audio) => audio.episode, - builder: (_, episode, __) { - return Row( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Padding( - padding: EdgeInsets.all(5.0), - ), - Container( - height: 30.0, - width: 30.0, - child: CircleAvatar( - backgroundImage: - FileImage(File("${episode.imagePath}")), - ), - ), - Spacer(), - Material( - color: Colors.transparent, - child: IconButton( - onPressed: () => Navigator.push( - context, - SlideUptRoute( - page: EpisodeDetail( - episodeItem: episode, - heroTag: 'playpanel')), - ), - icon: Icon(Icons.info), - ), - ), - Material( - color: Colors.transparent, - child: InkWell( - borderRadius: BorderRadius.only( - topRight: Radius.circular(10.0), - bottomRight: Radius.circular(10.0)), - child: Container( - height: 50.0, - width: 50.0, - child: Icon(Icons.keyboard_arrow_up)), - onTap: () => setState(() => _showlist = true)), - ), - ], - ); - }, - ), - ), + !_showTimer + // Setting sleep timer + ? Container( + height: 50.0, + margin: EdgeInsets.all(10.0), + decoration: BoxDecoration( + color: Theme.of(context).scaffoldBackgroundColor, + borderRadius: BorderRadius.all(Radius.circular(10.0)), + ), + child: Selector>( + selector: (_, audio) => Tuple3(audio.episode, + audio.stopOnComplete, audio.showStopWatch), + builder: (_, data, __) { + return Row( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Padding( + padding: EdgeInsets.all(5.0), + ), + Container( + height: 30.0, + width: 30.0, + child: CircleAvatar( + backgroundImage: FileImage( + File("${data.item1.imagePath}")), + ), + ), + Spacer(), + Material( + color: Colors.transparent, + child: !data.item3 + ? PopupMenuButton( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10))), + elevation: 1, + tooltip: 'Sleep Timer', + icon: Icon( + Icons.brightness_2, + color: data.item2 + ? Theme.of(context).accentColor + : Theme.of(context) + .iconTheme + .color, + ), + itemBuilder: (context) => [ + PopupMenuItem( + value: 1, + child: Text( + 'End of this episode')), + PopupMenuItem( + value: 2, + child: Text('Timer'), + ), + ], + onSelected: (value) { + if (value == 1) { + audio.setStopOnComplete = true; + } else if (value == 2) { + setState(() => _showTimer = true); + } + }) + : PopupMenuButton( + tooltip: 'Time Left', + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all( + Radius.circular(10))), + elevation: 1, + icon: Container( + alignment: Alignment.center, + height: 25, + width: 25, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: + Theme.of(context).accentColor, + ), + child: Text( + _timeLeft.toString(), + style: TextStyle( + color: Colors.white)), + ), + itemBuilder: (context) => [ + PopupMenuItem( + value: 1, + child: Text('Cancel')), + ], + onSelected: (value) { + audio.cancelTimer(); + _timer.cancel(); + setState(() => _timeLeft = 0); + }, + ), + ), + Material( + color: Colors.transparent, + child: IconButton( + onPressed: () => Navigator.push( + context, + SlideUptRoute( + page: EpisodeDetail( + episodeItem: data.item1, + heroTag: 'playpanel')), + ), + icon: Icon(Icons.info), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + borderRadius: BorderRadius.only( + topRight: Radius.circular(10.0), + bottomRight: Radius.circular(10.0)), + child: Container( + height: 50.0, + width: 50.0, + child: Icon(Icons.keyboard_arrow_up)), + onTap: () => + setState(() => _showlist = true)), + ), + ], + ); + }, + ), + ) + : _sleepTimer(context), ]), ), Container( @@ -283,8 +472,8 @@ class _PlayerWidgetState extends State { height: _showlist ? 300 : 0, width: MediaQuery.of(context).size.width, alignment: Alignment.center, - margin: EdgeInsets.all(20), - padding: EdgeInsets.only(bottom: 10.0), + // margin: EdgeInsets.all(20), + //padding: EdgeInsets.only(bottom: 10.0), decoration: BoxDecoration( borderRadius: BorderRadius.all(Radius.circular(10.0)), color: Theme.of(context).scaffoldBackgroundColor, diff --git a/lib/home/home.dart b/lib/home/home.dart index dfd5e85..0cd1b39 100644 --- a/lib/home/home.dart +++ b/lib/home/home.dart @@ -1,15 +1,45 @@ +import 'dart:io'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; +import 'package:tuple/tuple.dart'; import 'hometab.dart'; import 'package:tsacdop/home/appbar/importompl.dart'; import 'package:tsacdop/home/audioplayer.dart'; +import 'package:tsacdop/class/audiostate.dart'; import 'homescroll.dart'; -class Home extends StatelessWidget { +class Home extends StatefulWidget { + @override + _HomeState createState() => _HomeState(); +} + +class _HomeState extends State { + bool _loadPlay; + + static String _stringForSeconds(int seconds) { + if (seconds == null) return null; + return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; + } + + @override + void initState() { + super.initState(); + _loadPlay = false; + _getPlaylist(); + } + + _getPlaylist() async { + await Provider.of(context, listen: false).loadPlaylist(); + setState(() { + _loadPlay = true; + }); + } @override Widget build(BuildContext context) { + var audio = Provider.of(context, listen: false); return Stack(children: [ Column( mainAxisAlignment: MainAxisAlignment.start, @@ -22,8 +52,70 @@ class Home extends StatelessWidget { ), ], ), - Container( - child: PlayerWidget()), + AnimatedPositioned( + duration: Duration(milliseconds: 2000), + curve: Curves.elasticOut, + bottom: 50, + right: _loadPlay ? 5 : -25, + child: Container( + child: Selector>( + selector: (_, audio) => + Tuple3(audio.playerRunning, audio.queue, audio.lastPositin), + builder: (_, data, __) => !_loadPlay + ? Center() + : data.item1 || data.item2.playlist.length == 0 + ? Center() + : InkWell( + onTap: () => audio.playlistLoad(), + child: Stack( + alignment: Alignment.centerLeft, + children: [ + Container( + padding: EdgeInsets.only(left: 45, right: 10.0), + alignment: Alignment.centerRight, + decoration: BoxDecoration( + color: Theme.of(context).accentColor, + borderRadius: BorderRadius.only( + topLeft: Radius.circular(20.0), + bottomLeft: Radius.circular(20.0), + bottomRight: Radius.circular(10.0), + topRight: Radius.circular(10.0)), + boxShadow: [ + BoxShadow( + color: Theme.of(context).brightness == + Brightness.light + ? Colors.grey[400] + : Colors.grey[800], + blurRadius: 4, + offset: Offset(1, 1)), + ]), + height: 40, + child: Text(_stringForSeconds(data.item3) + '...', + style: TextStyle(color: Colors.white)), + ), + CircleAvatar( + radius: 20, + backgroundImage: FileImage(File( + "${data.item2.playlist.first.imagePath}")), + ), + Container( + height: 40.0, + width: 40, + decoration: BoxDecoration( + shape: BoxShape.circle, + color: Colors.black12), + child: Icon( + Icons.play_arrow, + color: Colors.white, + ), + ), + ], + ), + ), + ), + ), + ), + Container(child: PlayerWidget()), ]); } } diff --git a/lib/home/homescroll.dart b/lib/home/homescroll.dart index e167918..0570783 100644 --- a/lib/home/homescroll.dart +++ b/lib/home/homescroll.dart @@ -303,7 +303,7 @@ class _PodcastPreviewState extends State { @override Widget build(BuildContext context) { Color _c = (Theme.of(context).brightness == Brightness.light) - ? widget.podcastLocal.primaryColor.colorizedark() + ? widget.podcastLocal.primaryColor.colorizedark() : widget.podcastLocal.primaryColor.colorizeLight(); return Column( children: [ @@ -374,9 +374,41 @@ class ShowEpisode extends StatelessWidget { final List podcast; final PodcastLocal podcastLocal; ShowEpisode({Key key, this.podcast, this.podcastLocal}) : super(key: key); + @override Widget build(BuildContext context) { double _width = MediaQuery.of(context).size.width; + _showPopupMenu(Offset offset) async { + print(offset.dx); + double left = offset.dx; + double top = offset.dy; + await showMenu( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10))), + context: context, + position: RelativeRect.fromLTRB(left, top, _width - left, 0), + items: [ + PopupMenuItem( + child: Row( + children: [ + Icon(Icons.play_circle_outline), + Padding(padding: EdgeInsets.symmetric(horizontal: 2),), + Text('Play') + ], + ), + ), + PopupMenuItem(child: Row( + children: [ + Icon(Icons.favorite_border), + Padding(padding: EdgeInsets.symmetric(horizontal: 2),), + Text('Like') + ], + )), + ], + elevation: 8.0, + ); + } + return CustomScrollView( physics: const AlwaysScrollableScrollPhysics(), primary: false, @@ -392,11 +424,12 @@ class ShowEpisode extends StatelessWidget { ), delegate: SliverChildBuilderDelegate( (BuildContext context, int index) { - Color _c = - (Theme.of(context).brightness == Brightness.light) - ? podcastLocal.primaryColor.colorizedark() - : podcastLocal.primaryColor.colorizeLight(); - return InkWell( + Color _c = (Theme.of(context).brightness == Brightness.light) + ? podcastLocal.primaryColor.colorizedark() + : podcastLocal.primaryColor.colorizeLight(); + return GestureDetector( + onLongPressStart: (details) => _showPopupMenu(Offset( + details.globalPosition.dx, details.globalPosition.dy)), onTap: () { Navigator.push( context, diff --git a/lib/local_storage/key_value_storage.dart b/lib/local_storage/key_value_storage.dart index 5dc6a27..2911626 100644 --- a/lib/local_storage/key_value_storage.dart +++ b/lib/local_storage/key_value_storage.dart @@ -34,7 +34,6 @@ class KeyValueStorage { Future saveInt(int setting) async{ SharedPreferences prefs = await SharedPreferences.getInstance(); - print(setting.toString()); return prefs.setInt(key, setting); } diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index d20d3c1..83858f3 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -1,10 +1,8 @@ import 'package:sqflite/sqflite.dart'; import 'dart:async'; -import 'dart:io' as io; import 'package:path/path.dart'; import 'package:intl/intl.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; -import 'package:path_provider/path_provider.dart'; import 'package:dio/dio.dart'; import 'package:tsacdop/class/podcastlocal.dart'; import 'package:tsacdop/class/audiostate.dart'; @@ -20,8 +18,8 @@ class DBHelper { } initDb() async { - io.Directory documentsDirectory = await getApplicationDocumentsDirectory(); - String path = join(documentsDirectory.path, "podcasts.db"); + var documentsDirectory = await getDatabasesPath(); + String path = join(documentsDirectory, "podcasts.db"); Database theDb = await openDatabase(path, version: 1, onCreate: _onCreate); return theDb; } diff --git a/lib/main.dart b/lib/main.dart index 80c831c..3aad67a 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -32,15 +32,12 @@ Future main() async { callbackDispatcher, isInDebugMode: true, ); - Workmanager.registerPeriodicTask("update", "simplePeriodicTask", - frequency: Duration(hours: 1), - initialDelay: Duration(seconds: 10), + Workmanager.registerPeriodicTask("2", "update_podcasts", + frequency: Duration(minutes: 1), + initialDelay: Duration(seconds: 5), constraints: Constraints( networkType: NetworkType.connected, - requiresBatteryNotLow: true, - requiresCharging: false, - requiresDeviceIdle: true, - requiresStorageNotLow: true)); + )); await FlutterDownloader.initialize(); await SystemChrome.setPreferredOrientations( @@ -55,7 +52,7 @@ void callbackDispatcher() { await dbHelper.updatePodcastRss(podcastLocal); print('Refresh ' + podcastLocal.title); }); - return Future.value(true); + return true; }); } diff --git a/lib/podcasts/podcastgroup.dart b/lib/podcasts/podcastgroup.dart index 4358034..e3cc80a 100644 --- a/lib/podcasts/podcastgroup.dart +++ b/lib/podcasts/podcastgroup.dart @@ -1,10 +1,13 @@ import 'dart:io'; +import 'dart:math' as math; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:provider/provider.dart'; +import 'package:flutter_colorpicker/flutter_colorpicker.dart'; + import 'package:tsacdop/class/podcast_group.dart'; import 'package:tsacdop/class/podcastlocal.dart'; import 'package:tsacdop/podcasts/podcastdetail.dart'; @@ -18,97 +21,460 @@ class PodcastGroupList extends StatefulWidget { _PodcastGroupListState createState() => _PodcastGroupListState(); } -class _PodcastGroupListState extends State { - bool _loadSave; +class _PodcastGroupListState extends State + with SingleTickerProviderStateMixin { + bool _showSetting; + AnimationController _controller; + Animation _animation; + double _fraction; @override void initState() { super.initState(); - _loadSave = false; + _showSetting = false; + _fraction = 0; + _controller = AnimationController( + duration: const Duration(milliseconds: 500), vsync: this); + _animation = Tween(begin: 0.0, end: 1.0).animate(_controller) + ..addListener(() { + if (mounted) + setState(() { + _fraction = _animation.value; + }); + }); + _controller.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _controller.stop(); + } else if (status == AnimationStatus.dismissed) { + _controller.stop(); + } + }); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); } Widget _saveButton(BuildContext context) { var podcastList = widget.group.podcasts; - var _groupList = Provider.of(context); - return Container( - child: InkWell( - child: AnimatedContainer( - duration: Duration(milliseconds: 800), - width: _loadSave ? 70 : 0, - height: 60, - decoration: BoxDecoration( - color: Colors.blue, - shape: BoxShape.circle, - boxShadow: [ - BoxShadow( - color: Colors.grey[700], - blurRadius: 5, - offset: Offset(1, 1), - ), - ]), - alignment: Alignment.center, - child: Text( - 'Save', - style: TextStyle(color: Colors.white), - maxLines: 1, - )), - onTap: () async { - await _groupList.saveOrder(widget.group, podcastList); - Fluttertoast.showToast( - msg: 'Setting Saved', - gravity: ToastGravity.BOTTOM, - ); - setState(() { - _loadSave = false; - }); - }, + var _groupList = Provider.of(context, listen: false); + return Transform( + alignment: FractionalOffset(0.5, 0.5), + transform: Matrix4.rotationY(math.pi * _fraction), + child: Container( + child: InkWell( + child: Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: _fraction > 0.5 ? Colors.red : widget.group.getColor(), + shape: BoxShape.circle, + boxShadow: [ + BoxShadow( + color: Colors.grey[700], + blurRadius: 5, + offset: Offset(1, 1), + ), + ]), + alignment: Alignment.center, + child: Icon( + _fraction > 0.5 ? Icons.save : Icons.settings, + color: Colors.white, + )), + onTap: () async { + if (_fraction == 0) { + setState(() { + _showSetting = true; + }); + } else { + await _groupList.saveOrder(widget.group, podcastList); + Fluttertoast.showToast( + msg: 'Setting Saved', + gravity: ToastGravity.BOTTOM, + ); + _controller.reverse(); + } + }, + ), ), ); } @override Widget build(BuildContext context) { + var groupList = Provider.of(context, listen: false); return widget.group.podcastList.length == 0 ? Container( color: Theme.of(context).primaryColor, ) - : Container( - color: Theme.of(context).primaryColor, - child: Stack( - children: [ - ReorderableListView( - onReorder: (int oldIndex, int newIndex) { - setState(() { - if (newIndex > oldIndex) { - newIndex -= 1; - } - final PodcastLocal podcast = - widget.group.podcasts.removeAt(oldIndex); - widget.group.podcasts.insert(newIndex, podcast); - _loadSave = true; - }); - }, - children: widget.group.podcasts - .map((PodcastLocal podcastLocal) { - return Container( - decoration: - BoxDecoration(color: Theme.of(context).primaryColor), - key: ObjectKey(podcastLocal.title), - child: PodcastCard( - podcastLocal: podcastLocal, - group: widget.group, + : Stack( + children: [ + Container( + color: Theme.of(context).primaryColor, + child: Stack( + children: [ + ReorderableListView( + onReorder: (int oldIndex, int newIndex) { + setState(() { + if (newIndex > oldIndex) { + newIndex -= 1; + } + final PodcastLocal podcast = + widget.group.podcasts.removeAt(oldIndex); + widget.group.podcasts.insert(newIndex, podcast); + _controller.forward(); + }); + }, + children: widget.group.podcasts + .map((PodcastLocal podcastLocal) { + return Container( + decoration: BoxDecoration( + color: Theme.of(context).primaryColor), + key: ObjectKey(podcastLocal.title), + child: PodcastCard( + podcastLocal: podcastLocal, + group: widget.group, + ), + ); + }).toList(), + ), + Positioned( + bottom: 30, + right: 30, + child: _saveButton(context), + ), + ], + ), + ), + _showSetting + ? Positioned.fill( + child: GestureDetector( + onTap: () => setState(() => _showSetting = false), + child: Container( + color: Theme.of(context) + .scaffoldBackgroundColor + .withOpacity(0.5), + ), ), - ); - }).toList(), - ), - AnimatedPositioned( - duration: Duration(seconds: 1), - bottom: 30, - right: _loadSave ? 50 : 0, - child: _saveButton(context), - ), - ], - ), + ) + : Center(), + _showSetting + ? Container( + alignment: Alignment.bottomCenter, + child: Container( + height: 150.0, + alignment: Alignment.center, + decoration: BoxDecoration( + color: Theme.of(context).primaryColor, + boxShadow: [ + BoxShadow( + offset: Offset(0, -1), + blurRadius: 4, + color: Theme.of(context).brightness == + Brightness.light + ? Colors.grey[400] + : Colors.grey[800], + ), + ], + ), + child: SingleChildScrollView( + scrollDirection: Axis.vertical, + child: Container( + height: 150, + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + // mainAxisSize: MainAxisSize.min, + children: [ + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() => _showSetting = false); + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: + MaterialLocalizations.of(context) + .modalBarrierDismissLabel, + barrierColor: Colors.black54, + transitionDuration: + const Duration(milliseconds: 200), + pageBuilder: (BuildContext context, + Animation animaiton, + Animation + secondaryAnimation) => + AnnotatedRegion< + SystemUiOverlayStyle>( + value: SystemUiOverlayStyle( + statusBarIconBrightness: + Brightness.light, + systemNavigationBarColor: + Theme.of(context) + .brightness == + Brightness.light + ? Color.fromRGBO( + 113, + 113, + 113, + 1) + : Color.fromRGBO( + 15, 15, 15, 1), + statusBarColor: Theme.of( + context) + .brightness == + Brightness.light + ? Color.fromRGBO( + 113, 113, 113, 1) + : Color.fromRGBO( + 5, 5, 5, 1), + ), + child: SafeArea( + child: AlertDialog( + elevation: 1, + titlePadding: + EdgeInsets.only( + top: 20, + left: 40, + right: 200, + bottom: 20), + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.all( + Radius.circular( + 10.0))), + title: + Text('Choose a color'), + content: + SingleChildScrollView( + child: MaterialPicker( + onColorChanged: + (value) { + PodcastGroup newGroup = + PodcastGroup( + widget + .group.name, + color: value + .toString() + .substring( + 10, + 16), + id: widget + .group.id, + podcastList: + widget + .group + .podcastList); + groupList.updateGroup( + newGroup); + Navigator.of(context) + .pop(); + }, + pickerColor: + Colors.blue, + ), + ), + )))); + }, + child: Container( + height: 50.0, + padding: + EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Icon(Icons.colorize), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.0), + ), + Text('Change Color'), + ], + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() => _showSetting = false); + widget.group.name == 'Home' + ? Fluttertoast.showToast( + msg: + 'Home group is not supported', + gravity: ToastGravity.BOTTOM, + ) + : showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: + MaterialLocalizations.of( + context) + .modalBarrierDismissLabel, + barrierColor: Colors.black54, + transitionDuration: + const Duration( + milliseconds: 200), + pageBuilder: (BuildContext + context, + Animation animaiton, + Animation + secondaryAnimation) => + RenameGroup( + group: widget.group, + )); + }, + child: Container( + height: 50.0, + padding: + EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Icon(Icons.text_fields), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.0), + ), + Text('Rename'), + ], + ), + ), + ), + ), + Material( + color: Colors.transparent, + child: InkWell( + onTap: () { + setState(() => _showSetting = false); + widget.group.name == 'Home' + ? Fluttertoast.showToast( + msg: + 'Home group is not supported', + gravity: ToastGravity.BOTTOM, + ) + : showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: + MaterialLocalizations.of( + context) + .modalBarrierDismissLabel, + barrierColor: Colors.black54, + transitionDuration: + const Duration( + milliseconds: 200), + pageBuilder: (BuildContext + context, + Animation animaiton, + Animation + secondaryAnimation) => + AnnotatedRegion< + SystemUiOverlayStyle>( + value: SystemUiOverlayStyle( + statusBarIconBrightness: + Brightness.light, + systemNavigationBarColor: + Theme.of(context) + .brightness == + Brightness + .light + ? Color.fromRGBO( + 113, + 113, + 113, + 1) + : Color.fromRGBO( + 15, + 15, + 15, + 1), + statusBarColor: Theme.of( + context) + .brightness == + Brightness.light + ? Color.fromRGBO( + 113, 113, 113, 1) + : Color.fromRGBO( + 5, 5, 5, 1), + ), + child: SafeArea( + child: AlertDialog( + elevation: 1, + shape: RoundedRectangleBorder( + borderRadius: + BorderRadius.all( + Radius.circular( + 10.0))), + titlePadding: + EdgeInsets.only( + top: 20, + left: 20, + right: 200, + bottom: 20), + title: Text( + 'Delete confirm'), + content: Text( + 'Are you sure you want to delete this group? Podcasts will be moved to Home group.'), + actions: [ + FlatButton( + onPressed: () => + Navigator.of( + context) + .pop(), + child: Text( + 'CANCEL', + style: TextStyle( + color: Colors + .grey[ + 600]), + ), + ), + FlatButton( + onPressed: () { + groupList.delGroup( + widget.group); + Navigator.of( + context) + .pop(); + }, + child: Text( + 'CONFIRM', + style: TextStyle( + color: Colors + .red), + ), + ) + ], + ), + ), + )); + }, + child: Container( + height: 50, + padding: + EdgeInsets.symmetric(horizontal: 20), + child: Row( + children: [ + Icon(Icons.delete_outline), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5.0), + ), + Text('Delete'), + ], + ), + ), + ), + ), + ], + ), + ), + ), + ), + ) + : Center(), + ], ); } } @@ -330,7 +696,11 @@ class _PodcastCardState extends State { }); }), _buttonOnMenu(Icon(Icons.notifications), () {}), - _buttonOnMenu(Icon(Icons.remove_circle), () { + _buttonOnMenu( + Icon( + Icons.delete, + color: Colors.red, + ), () { showGeneralDialog( context: context, barrierDismissible: true, @@ -375,7 +745,11 @@ class _PodcastCardState extends State { FlatButton( onPressed: () => Navigator.of(context).pop(), - child: Text('CANCEL', style: TextStyle(color: Colors.grey[600]),), + child: Text( + 'CANCEL', + style: TextStyle( + color: Colors.grey[600]), + ), ), FlatButton( onPressed: () { @@ -404,3 +778,120 @@ class _PodcastCardState extends State { ); } } + +class RenameGroup extends StatefulWidget { + final PodcastGroup group; + RenameGroup({this.group, Key key}) : super(key: key); + @override + _RenameGroupState createState() => _RenameGroupState(); +} + +class _RenameGroupState extends State { + TextEditingController _controller; + String _newName; + int _error; + + @override + void initState() { + super.initState(); + _error = 0; + _controller = TextEditingController(); + } + + @override + void dispose() { + _controller.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + var groupList = Provider.of(context, listen: false); + List list = groupList.groups.map((e) => e.name).toList(); + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: Brightness.light, + systemNavigationBarColor: + Theme.of(context).brightness == Brightness.light + ? Color.fromRGBO(113, 113, 113, 1) + : Color.fromRGBO(5, 5, 5, 1), + statusBarColor: Theme.of(context).brightness == Brightness.light + ? Color.fromRGBO(113, 113, 113, 1) + : Color.fromRGBO(15, 15, 15, 1), + ), + child: SafeArea( + child: AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10))), + elevation: 1, + contentPadding: EdgeInsets.symmetric(horizontal: 20), + titlePadding: + EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20), + actionsPadding: EdgeInsets.all(0), + actions: [ + FlatButton( + onPressed: () => Navigator.of(context).pop(), + child: Text( + 'CANCEL', + style: TextStyle(color: Colors.grey[600]), + ), + ), + FlatButton( + onPressed: () async { + if (list.contains(_newName)) { + setState(() => _error = 1); + } else { + PodcastGroup newGroup = PodcastGroup(_newName, + color: widget.group.color, + id: widget.group.id, + podcastList: widget.group.podcastList); + groupList.updateGroup(newGroup); + Navigator.of(context).pop(); + } + }, + child: Text('DONE', + style: TextStyle(color: Theme.of(context).accentColor)), + ) + ], + title: Text('Create new group'), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + TextField( + decoration: InputDecoration( + contentPadding: EdgeInsets.symmetric(horizontal: 10), + hintText: 'New Group', + hintStyle: TextStyle(fontSize: 18), + filled: true, + focusedBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).accentColor, width: 2.0), + ), + enabledBorder: UnderlineInputBorder( + borderSide: BorderSide( + color: Theme.of(context).accentColor, width: 2.0), + ), + ), + cursorRadius: Radius.circular(2), + autofocus: true, + maxLines: 1, + controller: _controller, + onChanged: (value) { + _newName = value; + }, + ), + Container( + alignment: Alignment.centerLeft, + child: (_error == 1) + ? Text( + 'Group existed', + style: TextStyle(color: Colors.red[400]), + ) + : Center(), + ), + ], + ), + ), + )); + } +} diff --git a/lib/podcasts/podcastlist.dart b/lib/podcasts/podcastlist.dart index 26808fb..aa31da8 100644 --- a/lib/podcasts/podcastlist.dart +++ b/lib/podcasts/podcastlist.dart @@ -46,6 +46,9 @@ class _AboutPodcastState extends State { Widget build(BuildContext context) { var _groupList = Provider.of(context, listen: false); return AlertDialog( + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.all(Radius.circular(10.0))), + titlePadding: EdgeInsets.only(top: 20, left: 20, right: 200, bottom: 20), actions: [ FlatButton( padding: EdgeInsets.all(10.0), @@ -99,7 +102,7 @@ class _PodcastListState extends State { statusBarColor: Theme.of(context).primaryColor, ), child: SafeArea( - child: Scaffold( + child: Scaffold( appBar: AppBar( title: Text('Podcasts'), centerTitle: true, @@ -116,7 +119,8 @@ class _PodcastListState extends State { SliverPadding( padding: const EdgeInsets.all(10.0), sliver: SliverGrid( - gridDelegate: SliverGridDelegateWithFixedCrossAxisCount( + gridDelegate: + SliverGridDelegateWithFixedCrossAxisCount( childAspectRatio: 0.8, crossAxisCount: 3, ), @@ -133,12 +137,43 @@ class _PodcastListState extends State { ); }, onLongPress: () { - showDialog( - context: context, - builder: (BuildContext context) => - AboutPodcast( - podcastLocal: snapshot.data[index]), - ).then((_) => setState(() {})); + showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: + MaterialLocalizations.of(context) + .modalBarrierDismissLabel, + barrierColor: Colors.black54, + transitionDuration: + const Duration(milliseconds: 200), + pageBuilder: (BuildContext context, + Animation animaiton, + Animation secondaryAnimation) => + AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: + Brightness.light, + systemNavigationBarColor: + Theme.of(context) + .brightness == + Brightness.light + ? Color.fromRGBO( + 113, 113, 113, 1) + : Color.fromRGBO( + 15, 15, 15, 1), + statusBarColor: Theme.of(context) + .brightness == + Brightness.light + ? Color.fromRGBO( + 113, 113, 113, 1) + : Color.fromRGBO(5, 5, 5, 1), + ), + child: SafeArea( + child: AboutPodcast( + podcastLocal: + snapshot.data[index]), + ), + )); }, child: Container( alignment: Alignment.center, diff --git a/lib/podcasts/podcastmanage.dart b/lib/podcasts/podcastmanage.dart index a86dba0..0f90252 100644 --- a/lib/podcasts/podcastmanage.dart +++ b/lib/podcasts/podcastmanage.dart @@ -20,12 +20,6 @@ class _PodcastManageState extends State { )); } - @override - void initState() { - super.initState(); - } - - @override Widget build(BuildContext context) { return AnnotatedRegion( value: SystemUiOverlayStyle( @@ -34,85 +28,85 @@ class _PodcastManageState extends State { statusBarColor: Theme.of(context).primaryColor, ), child: SafeArea( - child: SafeArea( - child: Scaffold( - appBar: AppBar( - centerTitle: true, - title: Text('Groups'), - actions: [ - IconButton( - onPressed: () => showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: MaterialLocalizations.of(context) - .modalBarrierDismissLabel, - barrierColor: Colors.black54, - transitionDuration: const Duration(milliseconds: 200), - pageBuilder: (BuildContext context, Animation animaiton, - Animation secondaryAnimation) => - AddGroup()), - icon: Icon(Icons.add)), - OrderMenu(), - ], - ), - body: Consumer(builder: (_, groupList, __) { - bool _isLoading = groupList.isLoading; - List _groups = groupList.groups; - return _isLoading - ? Center() - : DefaultTabController( - length: _groups.length, - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Container( - height: 50, - padding: EdgeInsets.symmetric(horizontal: 10.0), - alignment: Alignment.centerLeft, - child: TabBar( - // labelColor: Colors.black, - // unselectedLabelColor: Colors.grey[500], - labelPadding: EdgeInsets.all(5.0), - indicator: getIndicator(), - isScrollable: true, - tabs: _groups.map((group) { - return Tab( + child: Scaffold( + appBar: AppBar( + centerTitle: true, + title: Text('Groups'), + actions: [ + IconButton( + onPressed: () => showGeneralDialog( + context: context, + barrierDismissible: true, + barrierLabel: MaterialLocalizations.of(context) + .modalBarrierDismissLabel, + barrierColor: Colors.black54, + transitionDuration: const Duration(milliseconds: 200), + pageBuilder: (BuildContext context, Animation animaiton, + Animation secondaryAnimation) => + AddGroup()), + icon: Icon(Icons.add)), + OrderMenu(), + ], + ), + body: Consumer(builder: (_, groupList, __) { + bool _isLoading = groupList.isLoading; + List _groups = groupList.groups; + return _isLoading + ? Center() + : DefaultTabController( + length: _groups.length, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + Container( + height: 50, + padding: EdgeInsets.symmetric(horizontal: 10.0), + alignment: Alignment.centerLeft, + child: TabBar( + labelColor: Colors.white, + unselectedLabelColor: Colors.black, + labelPadding: EdgeInsets.all(5.0), + indicator: getIndicator(), + isScrollable: true, + tabs: _groups.map((group) { + return Tab( child: Container( height: 30.0, - padding: EdgeInsets.symmetric( - horizontal: 10.0), + padding: + EdgeInsets.symmetric(horizontal: 10.0), alignment: Alignment.center, decoration: BoxDecoration( - color: Theme.of(context).brightness == - Brightness.light - ? Theme.of(context).primaryColorDark - : Colors.grey[800], - borderRadius: BorderRadius.all( - Radius.circular(15)), + color: group.getColor(), + // Theme.of(context).brightness == + // Brightness.light + // ? Theme.of(context).primaryColorDark + // : Colors.grey[800], + borderRadius: + BorderRadius.all(Radius.circular(15)), ), child: Text( group.name, )), ); + + }).toList(), + ), + ), + Expanded( + child: Container( + child: TabBarView( + children: _groups.map((group) { + return Container( + key: ObjectKey(group), + child: PodcastGroupList(group: group)); }).toList(), ), ), - Expanded( - child: Container( - child: TabBarView( - children: _groups.map((group) { - return Container( - key: ObjectKey(group), - child: PodcastGroupList(group: group)); - }).toList(), - ), - ), - ) - ], - )); - }), - ), + ) + ], + )); + }), ), ), ); @@ -214,7 +208,8 @@ class _AddGroupState extends State { Navigator.of(context).pop(); } }, - child: Text('DONE',style: TextStyle(color: Theme.of(context).accentColor)), + child: Text('DONE', + style: TextStyle(color: Theme.of(context).accentColor)), ) ], title: Text('Create new group'), @@ -228,10 +223,12 @@ class _AddGroupState extends State { hintStyle: TextStyle(fontSize: 18), filled: true, focusedBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).accentColor, width: 2.0), + borderSide: BorderSide( + color: Theme.of(context).accentColor, width: 2.0), ), enabledBorder: UnderlineInputBorder( - borderSide: BorderSide(color: Theme.of(context).accentColor, width: 2.0), + borderSide: BorderSide( + color: Theme.of(context).accentColor, width: 2.0), ), ), cursorRadius: Radius.circular(2), diff --git a/pubspec.lock b/pubspec.lock deleted file mode 100644 index a0c0c69..0000000 --- a/pubspec.lock +++ /dev/null @@ -1,481 +0,0 @@ -# Generated by pub -# See https://dart.dev/tools/pub/glossary#lockfile -packages: - archive: - dependency: transitive - description: - name: archive - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.11" - args: - dependency: transitive - description: - name: args - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.5.2" - async: - dependency: transitive - description: - name: async - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.0" - audiofileplayer: - dependency: "direct dev" - description: - name: audiofileplayer - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.1" - boolean_selector: - dependency: transitive - description: - name: boolean_selector - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.5" - cached_network_image: - dependency: "direct dev" - description: - name: cached_network_image - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.0" - charcode: - dependency: transitive - description: - name: charcode - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.2" - collection: - dependency: transitive - description: - name: collection - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.14.11" - color_thief_flutter: - dependency: "direct dev" - description: - name: color_thief_flutter - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.2" - convert: - dependency: transitive - description: - name: convert - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.1" - crypto: - dependency: transitive - description: - name: crypto - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.3" - csslib: - dependency: transitive - description: - name: csslib - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.16.1" - cupertino_icons: - dependency: "direct main" - description: - name: cupertino_icons - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.3" - dio: - dependency: "direct dev" - description: - name: dio - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.9" - file_picker: - dependency: "direct dev" - description: - name: file_picker - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.3+2" - flutter: - dependency: "direct main" - description: flutter - source: sdk - version: "0.0.0" - flutter_cache_manager: - dependency: transitive - description: - name: flutter_cache_manager - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.3" - flutter_colorpicker: - dependency: "direct dev" - description: - name: flutter_colorpicker - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.3.2" - flutter_downloader: - dependency: "direct dev" - description: - name: flutter_downloader - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.4.1" - flutter_html: - dependency: "direct dev" - description: - name: flutter_html - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.11.1" - flutter_test: - dependency: "direct dev" - description: flutter - source: sdk - version: "0.0.0" - flutter_web_plugins: - dependency: transitive - description: flutter - source: sdk - version: "0.0.0" - fluttertoast: - dependency: "direct dev" - description: - name: fluttertoast - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.3" - font_awesome_flutter: - dependency: "direct dev" - description: - name: font_awesome_flutter - url: "https://pub.flutter-io.cn" - source: hosted - version: "8.7.0" - google_fonts: - dependency: "direct dev" - description: - name: google_fonts - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.3.9" - html: - dependency: transitive - description: - name: html - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.14.0+3" - http: - dependency: transitive - description: - name: http - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.12.0+4" - http_parser: - dependency: transitive - description: - name: http_parser - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.1.3" - image: - dependency: "direct dev" - description: - name: image - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.1.4" - intl: - dependency: "direct dev" - description: - name: intl - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.16.1" - json_annotation: - dependency: "direct dev" - description: - name: json_annotation - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.0.1" - logging: - dependency: transitive - description: - name: logging - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.11.4" - marquee: - dependency: "direct dev" - description: - name: marquee - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.3.1" - matcher: - dependency: transitive - description: - name: matcher - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.12.6" - meta: - dependency: transitive - description: - name: meta - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.8" - nested: - dependency: transitive - description: - name: nested - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.0.4" - path: - dependency: transitive - description: - name: path - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.6.4" - path_provider: - dependency: "direct dev" - description: - name: path_provider - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.6.1" - pedantic: - dependency: transitive - description: - name: pedantic - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.8.0+1" - permission_handler: - dependency: "direct dev" - description: - name: permission_handler - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.2.0+hotfix.3" - petitparser: - dependency: transitive - description: - name: petitparser - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.4.0" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.1" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.2" - provider: - dependency: "direct dev" - description: - name: provider - url: "https://pub.flutter-io.cn" - source: hosted - version: "4.0.4" - quantize_dart: - dependency: transitive - description: - name: quantize_dart - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.3" - quiver: - dependency: transitive - description: - name: quiver - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.5" - shared_preferences: - dependency: "direct dev" - description: - name: shared_preferences - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.5.6+2" - shared_preferences_macos: - dependency: transitive - description: - name: shared_preferences_macos - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.0.1+6" - shared_preferences_platform_interface: - dependency: transitive - description: - name: shared_preferences_platform_interface - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.3" - shared_preferences_web: - dependency: transitive - description: - name: shared_preferences_web - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.2+4" - sky_engine: - dependency: transitive - description: flutter - source: sdk - version: "0.0.99" - source_span: - dependency: transitive - description: - name: source_span - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.5.5" - sqflite: - dependency: "direct dev" - description: - name: sqflite - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.2.1" - stack_trace: - dependency: transitive - description: - name: stack_trace - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.9.3" - stream_channel: - dependency: transitive - description: - name: stream_channel - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.0" - string_scanner: - dependency: transitive - description: - name: string_scanner - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.5" - synchronized: - dependency: transitive - description: - name: synchronized - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.2.0" - term_glyph: - dependency: transitive - description: - name: term_glyph - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.0" - test_api: - dependency: transitive - description: - name: test_api - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.11" - tuple: - dependency: "direct dev" - description: - name: tuple - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.3" - typed_data: - dependency: transitive - description: - name: typed_data - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.1.6" - url_launcher: - dependency: "direct dev" - description: - name: url_launcher - url: "https://pub.flutter-io.cn" - source: hosted - version: "5.4.2" - url_launcher_macos: - dependency: transitive - description: - name: url_launcher_macos - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.0.1+4" - url_launcher_platform_interface: - dependency: transitive - description: - name: url_launcher_platform_interface - url: "https://pub.flutter-io.cn" - source: hosted - version: "1.0.6" - url_launcher_web: - dependency: transitive - description: - name: url_launcher_web - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.1.1+1" - uuid: - dependency: "direct dev" - description: - name: uuid - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.4" - vector_math: - dependency: transitive - description: - name: vector_math - url: "https://pub.flutter-io.cn" - source: hosted - version: "2.0.8" - workmanager: - dependency: "direct dev" - description: - name: workmanager - url: "https://pub.flutter-io.cn" - source: hosted - version: "0.2.0" - xml: - dependency: "direct dev" - description: - name: xml - url: "https://pub.flutter-io.cn" - source: hosted - version: "3.5.0" -sdks: - dart: ">=2.6.0 <3.0.0" - flutter: ">=1.12.13+hotfix.4 <2.0.0" diff --git a/pubspec.yaml b/pubspec.yaml index ca634bd..73282e7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -32,18 +32,18 @@ dev_dependencies: flutter_html: ^0.11.1 path_provider: ^1.6.1 color_thief_flutter: ^1.0.1 - provider: ^4.0.1 - google_fonts: ^0.3.2 + provider: ^4.0.4 + google_fonts: ^0.3.9 dio: ^3.0.9 - file_picker: ^1.2.0 + file_picker: ^1.4.3+2 xml: ^3.5.0 marquee: ^1.3.1 audiofileplayer: ^1.1.1 flutter_downloader: ^1.4.1 - permission_handler: ^4.2.0+hotfix.3 + permission_handler: ^4.3.0 fluttertoast: ^3.1.3 intl: ^0.16.1 - url_launcher: ^5.4.1 + url_launcher: ^5.4.2 image: ^2.1.4 shared_preferences: ^0.5.6+1 uuid: ^2.0.4 @@ -52,6 +52,7 @@ dev_dependencies: workmanager: ^0.2.0 font_awesome_flutter: ^8.7.0 flutter_colorpicker: ^0.3.2 + lazy_loading_list: ^1.0.0+1