diff --git a/android/app/build.gradle b/android/app/build.gradle index eedf618..2f45f64 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -49,7 +49,7 @@ android { applicationId "com.stonegate.tsacdop" minSdkVersion 19 targetSdkVersion 29 - versionCode 16 + versionCode 17 versionName flutterVersionName testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" } diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index 0e046e8..f8b3528 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -55,10 +55,12 @@ class _EpisodeDetailState extends State { setState(() { _showMenu = true; }); - } else + } else if (_controller.offset < + _controller.position.maxScrollExtent * 0.8) { setState(() { _showMenu = false; }); + } } _launchUrl(String url) async { @@ -113,7 +115,7 @@ class _EpisodeDetailState extends State { PopupMenuButton( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))), - elevation: 2, + elevation: 1, tooltip: 'Menu', itemBuilder: (context) => [ PopupMenuItem( @@ -152,6 +154,8 @@ class _EpisodeDetailState extends State { gravity: ToastGravity.BOTTOM, ); break; + default: + break; } }, ), @@ -323,8 +327,7 @@ class _EpisodeDetailState extends State { 'Still no shownote received\n for this episode.', textAlign: TextAlign.center, style: TextStyle( - color: context.textTheme - .bodyText1.color + color: context.textColor .withOpacity(0.5))), ], ), @@ -381,7 +384,7 @@ class MenuBar extends StatefulWidget { } class _MenuBarState extends State { - bool _liked; + bool _liked = false; Future getPosition(EpisodeBrief episode) async { var dbHelper = DBHelper(); @@ -401,7 +404,6 @@ class _MenuBarState extends State { if (result == 1 && mounted) setState(() { _liked = false; - // _like = 0; }); return result; } @@ -526,12 +528,10 @@ class _MenuBarState extends State { }, ), DownloadButton(episode: widget.episodeItem), - Selector>( - selector: (_, audio) => audio.queue.playlist - .map((e) => e.enclosureUrl) - .toList(), + Selector>( + selector: (_, audio) => audio.queue.playlist, builder: (_, data, __) { - return data.contains(widget.episodeItem.enclosureUrl) + return data.contains(widget.episodeItem) ? _buttonOnMenu( Icon(Icons.playlist_add_check, color: Theme.of(context).accentColor), () { diff --git a/lib/episodes/episodedownload.dart b/lib/episodes/episodedownload.dart index c93477e..f72de9d 100644 --- a/lib/episodes/episodedownload.dart +++ b/lib/episodes/episodedownload.dart @@ -2,7 +2,6 @@ import 'dart:ui'; import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:provider/provider.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:permission_handler/permission_handler.dart'; @@ -13,6 +12,7 @@ import '../state/download_state.dart'; import '../state/audiostate.dart'; import '../state/settingstate.dart'; import '../type/episodebrief.dart'; +import '../util/general_dialog.dart'; class DownloadButton extends StatefulWidget { final EpisodeBrief episode; @@ -107,54 +107,31 @@ class _DownloadButtonState extends State { Future _useDataConfirem() async { bool ifUseData = false; - await showGeneralDialog( - context: context, - barrierDismissible: true, - barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, - barrierColor: Colors.black54, - transitionDuration: const Duration(milliseconds: 200), - pageBuilder: (BuildContext context, Animation animaiton, - Animation secondaryAnimation) => - AnnotatedRegion( - value: SystemUiOverlayStyle( - statusBarIconBrightness: Brightness.light, - systemNavigationBarColor: - Theme.of(context).brightness == Brightness.light - ? Color.fromRGBO(113, 113, 113, 1) - : Color.fromRGBO(15, 15, 15, 1), + await generalDialog( + context, + title: Text('Cellular data warn'), + content: Text('Are you sure you want to use cellular data to download?'), + actions: [ + FlatButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + 'CANCEL', + style: TextStyle(color: Colors.grey[600]), + ), ), - child: AlertDialog( - elevation: 1, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all(Radius.circular(10.0))), - titlePadding: - EdgeInsets.only(top: 20, left: 20, right: 100, bottom: 20), - title: Text('Cellular data warn'), - content: - Text('Are you sure you want to use cellular data to download?'), - actions: [ - FlatButton( - onPressed: () { - Navigator.of(context).pop(); - }, - child: Text( - 'CANCEL', - style: TextStyle(color: Colors.grey[600]), - ), - ), - FlatButton( - onPressed: () { - ifUseData = true; - Navigator.of(context).pop(); - }, - child: Text( - 'CONFIRM', - style: TextStyle(color: Colors.red), - ), - ) - ], - ), - ), + FlatButton( + onPressed: () { + ifUseData = true; + Navigator.of(context).pop(); + }, + child: Text( + 'CONFIRM', + style: TextStyle(color: Colors.red), + ), + ) + ], ); return ifUseData; } diff --git a/lib/generated/l10n.dart b/lib/generated/l10n.dart index 886dd9d..4e2b9d0 100644 --- a/lib/generated/l10n.dart +++ b/lib/generated/l10n.dart @@ -8,9 +8,13 @@ import 'intl/messages_all.dart'; // Made by Localizely // ************************************************************************** +// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars + class S { S(); + static S current; + static const AppLocalizationDelegate delegate = AppLocalizationDelegate(); @@ -19,7 +23,9 @@ class S { final localeName = Intl.canonicalizedLocale(name); return initializeMessages(localeName).then((_) { Intl.defaultLocale = localeName; - return S(); + S.current = S(); + + return S.current; }); } diff --git a/lib/home/about.dart b/lib/home/about.dart index 64f68ad..a18a86e 100644 --- a/lib/home/about.dart +++ b/lib/home/about.dart @@ -6,7 +6,7 @@ import 'package:line_icons/line_icons.dart'; import '../util/context_extension.dart'; -const String version = '0.3.3'; +const String version = '0.3.4'; class AboutApp extends StatelessWidget { _launchUrl(String url) async { diff --git a/lib/home/addpodcast.dart b/lib/home/addpodcast.dart index 2d41f88..51fd1f2 100644 --- a/lib/home/addpodcast.dart +++ b/lib/home/addpodcast.dart @@ -238,7 +238,7 @@ class _SearchListState extends State { Future _getList(String searchText, int nextOffset) async { String apiKey = environment['apiKey']; String url = "https://listen-api.listennotes.com/api/v2/search?q=" + - searchText + + Uri.encodeComponent(searchText) + "&sort_by_date=0&type=podcast&offset=$nextOffset"; Response response = await Dio().get(url, options: Options(headers: { diff --git a/lib/home/home.dart b/lib/home/home.dart index 7c13677..9e58d86 100644 --- a/lib/home/home.dart +++ b/lib/home/home.dart @@ -1050,168 +1050,183 @@ class _MyFavoriteState extends State<_MyFavorite> @override Widget build(BuildContext context) { super.build(context); - return FutureBuilder>( - future: _getLikedRssItem(_top, _sortBy), - builder: (context, snapshot) { - if (snapshot.hasError) print(snapshot.error); - return (snapshot.hasData) - ? snapshot.data.length == 0 - ? Padding( - padding: EdgeInsets.only(top: 150), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - children: [ - Icon(LineIcons.heartbeat_solid, - size: 80, color: Colors.grey[500]), - Padding(padding: EdgeInsets.symmetric(vertical: 10)), - Text( - 'No episode collected yet', - style: TextStyle(color: Colors.grey[500]), + return Selector( + selector: (_, audio) => audio.episodeState, + builder: (context, episodeState, child) { + return FutureBuilder>( + future: _getLikedRssItem(_top, _sortBy), + builder: (context, snapshot) { + if (snapshot.hasError) print(snapshot.error); + return (snapshot.hasData) + ? snapshot.data.length == 0 + ? Padding( + padding: EdgeInsets.only(top: 150), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Icon(LineIcons.heartbeat_solid, + size: 80, color: Colors.grey[500]), + Padding( + padding: EdgeInsets.symmetric(vertical: 10)), + Text( + 'No episode collected yet', + style: TextStyle(color: Colors.grey[500]), + ) + ], + ), ) - ], - ), - ) - : NotificationListener( - onNotification: (ScrollNotification scrollInfo) { - if (scrollInfo.metrics.pixels == - scrollInfo.metrics.maxScrollExtent && - snapshot.data.length == _top) _loadMoreEpisode(); - return true; - }, - 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( + : NotificationListener( + onNotification: (ScrollNotification scrollInfo) { + if (scrollInfo.metrics.pixels == + scrollInfo.metrics.maxScrollExtent && + snapshot.data.length == _top) + _loadMoreEpisode(); + return true; + }, + 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: 5), + 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'), ), - Icon( - _sortBy == 0 - ? LineIcons - .cloud_download_alt_solid - : LineIcons.heartbeat_solid, - size: 18, + PopupMenuItem( + value: 1, + child: Text('Like Date'), ) ], - )), - itemBuilder: (context) => [ - PopupMenuItem( - value: 0, - child: Text('Update 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.one; + }); + else if (_layout == Layout.two) + setState(() { + _layout = Layout.three; + }); + else + setState(() { + _layout = Layout.two; + }); + }, + icon: _layout == Layout.three + ? SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 0, + context + .textTheme + .bodyText1 + .color), + ), + ) + : _layout == Layout.two + ? SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 1, + context + .textTheme + .bodyText1 + .color), + ), + ) + : SizedBox( + height: 10, + width: 30, + child: CustomPaint( + painter: LayoutPainter( + 4, + context + .textTheme + .bodyText1 + .color), + ), + )), ), - 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.one; - }); - else if (_layout == Layout.two) - setState(() { - _layout = Layout.three; - }); - else - setState(() { - _layout = Layout.two; - }); - }, - icon: _layout == Layout.three - ? SizedBox( - height: 10, - width: 30, - child: CustomPaint( - painter: LayoutPainter( - 0, - context.textTheme - .bodyText1.color), - ), - ) - : _layout == Layout.two - ? SizedBox( - height: 10, - width: 30, - child: CustomPaint( - painter: LayoutPainter( - 1, - context.textTheme - .bodyText1.color), - ), - ) - : SizedBox( - height: 10, - width: 30, - child: CustomPaint( - painter: LayoutPainter( - 4, - context.textTheme - .bodyText1.color), - ), - )), - ), - ], - )), - ), - EpisodeGrid( - episodes: snapshot.data, - layout: _layout, - initNum: 0, - ), - SliverList( - delegate: SliverChildBuilderDelegate( - (BuildContext context, int index) { - return _loadMore - ? Container( - height: 2, - child: LinearProgressIndicator()) - : Center(); - }, - childCount: 1, + )), + ), + EpisodeGrid( + episodes: snapshot.data, + layout: _layout, + initNum: 0, + ), + SliverList( + delegate: SliverChildBuilderDelegate( + (BuildContext context, int index) { + return _loadMore + ? Container( + height: 2, + child: LinearProgressIndicator()) + : Center(); + }, + childCount: 1, + ), + ), + ], ), - ), - ], - ), - ) - : Center(); - }, - ); + ) + : Center(); + }, + ); + }); } @override diff --git a/lib/home/home_groups.dart b/lib/home/home_groups.dart index 7273968..5189888 100644 --- a/lib/home/home_groups.dart +++ b/lib/home/home_groups.dart @@ -12,6 +12,7 @@ import 'package:feature_discovery/feature_discovery.dart'; import '../type/episodebrief.dart'; import '../state/podcast_group.dart'; import '../state/subscribe_podcast.dart'; +import '../state/download_state.dart'; import '../type/podcastlocal.dart'; import '../state/audiostate.dart'; import '../util/custompaint.dart'; @@ -19,6 +20,7 @@ import '../util/pageroute.dart'; import '../util/colorize.dart'; import '../util/context_extension.dart'; import '../local_storage/sqflite_localpodcast.dart'; +import '../local_storage/key_value_storage.dart'; import '../episodes/episodedetail.dart'; import '../podcasts/podcastdetail.dart'; import '../podcasts/podcastmanage.dart'; @@ -478,11 +480,61 @@ class ShowEpisode extends StatelessWidget { return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; } + 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); + } + + Future> _getEpisodeMenu() async { + KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey); + List list = await popupMenuStorage.getMenu(); + return list; + } + + Future _isDownloaded(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + return await dbHelper.isDownloaded(episode.enclosureUrl); + } + + _markListened(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + bool marked = await dbHelper.checkMarked(episode); + if (!marked) { + final PlayHistory history = + PlayHistory(episode.title, episode.enclosureUrl, 0, 1); + await dbHelper.saveHistory(history); + } + } + + Future _saveLiked(String url) async { + var dbHelper = DBHelper(); + int result = await dbHelper.setLiked(url); + return result; + } + + Future _setUnliked(String url) async { + var dbHelper = DBHelper(); + int result = await dbHelper.setUniked(url); + return result; + } + _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, bool isPlaying, bool isInPlaylist) async { var audio = Provider.of(context, listen: false); double left = offset.dx; double top = offset.dy; + bool isLiked, isDownload; + int isListened; + var downloader = Provider.of(context, listen: false); + List menuList = await _getEpisodeMenu(); + if (menuList.contains(3)) isListened = await _isListened(episode); + if (menuList.contains(2)) isLiked = await _isLiked(episode); + if (menuList.contains(4)) isDownload = await _isDownloaded(episode); await showMenu( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))), @@ -506,39 +558,134 @@ class ShowEpisode extends StatelessWidget { ], ), ), - PopupMenuItem( - value: 1, - child: Row( - children: [ - Icon( - LineIcons.clock_solid, - color: Colors.red, - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 2), - ), - !isInPlaylist ? Text('Later') : Text('Remove') - ], - )), + menuList.contains(1) + ? PopupMenuItem( + value: 1, + child: Row( + children: [ + Icon( + LineIcons.clock_solid, + color: Colors.red, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + !isInPlaylist ? Text('Later') : Text('Remove') + ], + )) + : null, + menuList.contains(2) + ? PopupMenuItem( + value: 2, + child: Row( + children: [ + Icon(LineIcons.heart, color: Colors.red, size: 21), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + isLiked + ? Text( + 'Unlike', + ) + : Text('Like') + ], + )) + : null, + menuList.contains(3) + ? PopupMenuItem( + value: 3, + child: Row( + children: [ + SizedBox( + width: 23, + height: 23, + child: CustomPaint( + painter: + ListenedAllPainter(Colors.blue, stroke: 1.5)), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + isListened > 0.95 + ? Text('Listened', + style: TextStyle( + color: context.textColor.withOpacity(0.5))) + : Text('Mark\nListened') + ], + )) + : null, + menuList.contains(4) + ? PopupMenuItem( + value: 4, + child: Row( + children: [ + Icon(LineIcons.download_solid, color: Colors.green), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + isDownload + ? Text('Downloaded', + style: TextStyle( + color: context.textColor.withOpacity(0.5))) + : Text('Download') + ], + )) + : null, ], elevation: 5.0, - ).then((value) { - if (value == 0) { - if (!isPlaying) audio.episodeLoad(episode); - } else if (value == 1) { - if (!isInPlaylist) { - audio.addToPlaylist(episode); - Fluttertoast.showToast( - msg: 'Added to playlist', - gravity: ToastGravity.BOTTOM, - ); - } else { - audio.delFromPlaylist(episode); - Fluttertoast.showToast( - msg: 'Removed from playlist', - gravity: ToastGravity.BOTTOM, - ); - } + ).then((value) async { + switch (value) { + case 0: + if (!isPlaying) audio.episodeLoad(episode); + break; + case 1: + if (!isInPlaylist) { + audio.addToPlaylist(episode); + Fluttertoast.showToast( + msg: 'Added to playlist', + gravity: ToastGravity.BOTTOM, + ); + } else { + audio.delFromPlaylist(episode); + Fluttertoast.showToast( + msg: 'Removed from playlist', + gravity: ToastGravity.BOTTOM, + ); + } + break; + case 2: + if (isLiked) { + await _setUnliked(episode.enclosureUrl); + audio.setEpisodeState = true; + Fluttertoast.showToast( + msg: 'Unliked', + gravity: ToastGravity.BOTTOM, + ); + } else { + await _saveLiked(episode.enclosureUrl); + audio.setEpisodeState = true; + Fluttertoast.showToast( + msg: 'Liked', + gravity: ToastGravity.BOTTOM, + ); + } + break; + case 3: + if (isListened < 0.95) { + await _markListened(episode); + audio.setEpisodeState = true; + Fluttertoast.showToast( + msg: 'Mark listened', + gravity: ToastGravity.BOTTOM, + ); + } + break; + + case 4: + if (!isDownload) downloader.startTask(episode); + break; + default: + break; } }); } diff --git a/lib/local_storage/key_value_storage.dart b/lib/local_storage/key_value_storage.dart index 801d206..ac1701c 100644 --- a/lib/local_storage/key_value_storage.dart +++ b/lib/local_storage/key_value_storage.dart @@ -22,6 +22,8 @@ const String podcastLayoutKey = 'podcastLayoutKey'; const String recentLayoutKey = 'recentLayoutKey'; const String favLayoutKey = 'favLayoutKey'; const String downloadLayoutKey = 'downloadLayoutKey'; +const String autoDownloadNetworkKey = 'autoDownloadNetwork'; +const String episodePopupMenuKey = 'episodePopupMenuKey'; class KeyValueStorage { final String key; @@ -88,4 +90,18 @@ class KeyValueStorage { } return prefs.getString(key); } + + saveMenu(List list) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setStringList(key, list.map((e) => e.toString()).toList()); + } + + Future> getMenu() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + if (prefs.getStringList(key) == null) { + await prefs.setStringList(key, ['0', '1', '12', '13', '14']); + } + List list = prefs.getStringList(key); + return list.map((e) => int.parse(e)).toList(); + } } diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index 535c213..f2e1afc 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -382,7 +382,6 @@ class DBHelper { String month = mmDd.stringMatch(pubDate); date = DateFormat('yyyy-MM-dd HH:mm', 'en_US') .parse(month + ' ' + time); - print(month); print(date.toString()); } else { date = DateTime.now(); @@ -832,36 +831,36 @@ class DBHelper { return episodes; } - Future> gettNewRssItem(String id) async { - var dbClient = await database; - List episodes = []; - 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, P.skip_seconds - FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id - WHERE is_new = 1 AND downloaded != 'ND' AND P.id = ?ORDER BY E.milliseconds DESC """, - [id], - ); - 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'], - list[x]['skip_seconds'])); - } - return episodes; - } + //Future> getNewRssItem(String id) async { + // var dbClient = await database; + // List episodes = []; + // 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, P.skip_seconds + // FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + // WHERE is_new = 1 AND downloaded != 'ND' AND P.id = ?ORDER BY E.milliseconds DESC """, + // [id], + // ); + // 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'], + // list[x]['skip_seconds'])); + // } + // return episodes; + //} Future removeAllNewMark() async { var dbClient = await database; @@ -994,6 +993,13 @@ class DBHelper { return list.first['liked'] == 0 ? false : true; } + Future isDownloaded(String url) async { + var dbClient = await database; + List list = await dbClient + .rawQuery("SELECT downloaded FROM Episodes WHERE enclosure_url = ?", [url]); + return list.first['downloaded'] == 'ND' ? 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 e73c767..b923e9b 100644 --- a/lib/podcasts/podcastdetail.dart +++ b/lib/podcasts/podcastdetail.dart @@ -1,10 +1,12 @@ import 'dart:io'; import 'dart:async'; +import 'package:connectivity/connectivity.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:html/parser.dart'; +import 'package:tsacdop/state/download_state.dart'; import 'package:url_launcher/url_launcher.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:flutter_linkify/flutter_linkify.dart'; @@ -56,6 +58,26 @@ class _PodcastDetailState extends State { msg: 'Updated $result Episodes', gravity: ToastGravity.TOP, ); + bool autoDownload = await dbHelper.getAutoDownload(podcastLocal.id); + if (autoDownload) { + var result = await Connectivity().checkConnectivity(); + KeyValueStorage autoDownloadStorage = + KeyValueStorage(autoDownloadNetworkKey); + int autoDownloadNetwork = await autoDownloadStorage.getInt(); + if (autoDownloadNetwork == 1) { + List episodes = + await dbHelper.getNewEpisodes(podcastLocal.id); + episodes.forEach((episode) { + DownloadState().startTask(episode, showNotification: false); + }); + } else if (result == ConnectivityResult.wifi) { + List episodes = + await dbHelper.getNewEpisodes(podcastLocal.id); + episodes.forEach((episode) { + DownloadState().startTask(episode, showNotification: false); + }); + } + } // Provider.of(context, listen: false) // .updatePodcast(podcastLocal.id); } else { diff --git a/lib/podcasts/podcastgroup.dart b/lib/podcasts/podcastgroup.dart index 1c7962e..ecc23c1 100644 --- a/lib/podcasts/podcastgroup.dart +++ b/lib/podcasts/podcastgroup.dart @@ -5,14 +5,12 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; -import 'package:line_icons/line_icons.dart'; import '../state/podcast_group.dart'; import '../type/podcastlocal.dart'; import '../local_storage/sqflite_localpodcast.dart'; -import '../podcasts/podcastdetail.dart'; -import '../util/pageroute.dart'; import '../util/colorize.dart'; import '../util/duraiton_picker.dart'; import '../util/context_extension.dart'; @@ -98,8 +96,11 @@ class _PodcastCardState extends State } _setAutoDownload(String id, bool boo) async { - DBHelper dbHelper = DBHelper(); - await dbHelper.saveAutoDownload(id, boo); + bool permission = await _checkPermmison(); + if (permission) { + DBHelper dbHelper = DBHelper(); + await dbHelper.saveAutoDownload(id, boo); + } } Future _getAutoDownload(String id) async { @@ -107,6 +108,21 @@ class _PodcastCardState extends State return await dbHelper.getAutoDownload(id); } + Future _checkPermmison() async { + PermissionStatus permission = await Permission.storage.status; + if (permission != PermissionStatus.granted) { + Map permissions = + await [Permission.storage].request(); + if (permissions[Permission.storage] == PermissionStatus.granted) { + return true; + } else { + return false; + } + } else { + return true; + } + } + String _stringForSeconds(double seconds) { if (seconds == null) return null; return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; @@ -344,13 +360,27 @@ class _PodcastCardState extends State initialData: false, builder: (context, snapshot) { return _buttonOnMenu( - icon: Icon( - LineIcons.cloud_download_alt_solid, - size: _value == 0 ? 1 : 20 * _value, - color: snapshot.data - ? context.accentColor - : null), - tooltip: 'AutoDownload', + icon: Container( + child: Icon(Icons.done_all, + size: _value * 15, + color: snapshot.data + ? Colors.white + : null), + height: _value == 0 ? 1 : 18 * _value, + width: _value == 0 ? 1 : 18 * _value, + decoration: BoxDecoration( + border: Border.all( + width: 1, + color: snapshot.data + ? context.accentColor + : context.textTheme.subtitle1 + .color), + shape: BoxShape.circle, + color: snapshot.data + ? context.accentColor + : null), + ), + tooltip: 'Auto Download', onTap: () { _setAutoDownload(widget.podcastLocal.id, !snapshot.data); diff --git a/lib/settings/downloads_manage.dart b/lib/settings/downloads_manage.dart index 4b36eb5..e194d8c 100644 --- a/lib/settings/downloads_manage.dart +++ b/lib/settings/downloads_manage.dart @@ -58,7 +58,6 @@ class _DownloadsManageState extends State { _delSelectedEpisodes() async { setState(() => _clearing = true); await Future.forEach(_selectedList, (EpisodeBrief episode) async { - print(episode.downloaded); await FlutterDownloader.remove( taskId: episode.downloaded, shouldDeleteContent: true); var dbHelper = DBHelper(); @@ -212,7 +211,6 @@ class _DownloadsManageState extends State { value: _selectedList .contains(_episodes[index]), onChanged: (bool boo) { - print(boo); if (boo) { setState(() => _selectedList .add(_episodes[index])); diff --git a/lib/settings/history.dart b/lib/settings/history.dart index eaf9788..7ea3062 100644 --- a/lib/settings/history.dart +++ b/lib/settings/history.dart @@ -117,7 +117,6 @@ class _PlayedHistoryState extends State systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarIconBrightness: Theme.of(context).accentColorBrightness, - //statusBarColor: Theme.of(context).primaryColor, ), child: Scaffold( backgroundColor: Theme.of(context).primaryColor, diff --git a/lib/settings/layouts.dart b/lib/settings/layouts.dart index de51e5e..45d43eb 100644 --- a/lib/settings/layouts.dart +++ b/lib/settings/layouts.dart @@ -5,6 +5,7 @@ import '../util/context_extension.dart'; import '../util/episodegrid.dart'; import '../util/custompaint.dart'; import '../local_storage/key_value_storage.dart'; +import 'popup_menu.dart'; class LayoutSetting extends StatefulWidget { const LayoutSetting({Key key}) : super(key: key); @@ -23,7 +24,7 @@ class _LayoutSettingState extends State { Widget _gridOptions(BuildContext context, {String key, Layout layout, Layout option, double scale}) => Padding( - padding: const EdgeInsets.only(top: 10.0, bottom: 10.0, left: 20.0), + padding: const EdgeInsets.only(top: 10.0, bottom: 10.0), child: InkWell( onTap: () async { KeyValueStorage storage = KeyValueStorage(key); @@ -34,7 +35,9 @@ class _LayoutSettingState extends State { child: Container( height: 30, width: 50, - color: layout == option ? context.accentColor : Colors.transparent, + color: layout == option + ? context.accentColor + : context.primaryColorDark, alignment: Alignment.center, child: SizedBox( height: 10, @@ -80,6 +83,32 @@ class _LayoutSettingState extends State { }); } + Widget _setDefaultGridView(BuildContext context, {String text, String key}) { + return Padding( + padding: EdgeInsets.only(left: 80.0, right: 20, bottom: 10), + child: context.width > 360 + ? Row( + children: [ + Text( + text, + ), + Spacer(), + _setDefaultGrid(context, key: key), + ], + ) + : Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + text, + ), + _setDefaultGrid(context, key: key), + ], + ), + ); + } + @override Widget build(BuildContext context) { return AnnotatedRegion( @@ -106,7 +135,7 @@ class _LayoutSettingState extends State { height: 30.0, padding: EdgeInsets.symmetric(horizontal: 80), alignment: Alignment.centerLeft, - child: Text('Default grid view', + child: Text('Episode popup menu', style: Theme.of(context) .textTheme .bodyText1 @@ -118,44 +147,42 @@ class _LayoutSettingState extends State { scrollDirection: Axis.vertical, children: [ ListTile( - contentPadding: - EdgeInsets.only(left: 80.0, right: 20, bottom: 10), - // leading: Icon(Icons.colorize), - title: Text( - 'Podcast page', - ), - subtitle: - _setDefaultGrid(context, key: podcastLayoutKey), - ), - ListTile( - contentPadding: - EdgeInsets.only(left: 80.0, right: 20, bottom: 10), - // leading: Icon(Icons.colorize), - title: Text( - 'Recent tab in homepage', - ), - subtitle: - _setDefaultGrid(context, key: recentLayoutKey), - ), - ListTile( - contentPadding: - EdgeInsets.only(left: 80.0, right: 20, bottom: 10), - // leading: Icon(Icons.colorize), - title: Text( - 'Favorite tab in homepage', - ), - subtitle: _setDefaultGrid(context, key: favLayoutKey), - ), - ListTile( - contentPadding: - EdgeInsets.only(left: 80.0, right: 20, bottom: 10), - // leading: Icon(Icons.colorize), - title: Text( - 'Download tab in homepage', - ), - subtitle: _setDefaultGrid(context, key: downloadLayoutKey), + onTap: () => Navigator.push( + context, + MaterialPageRoute( + builder: (context) => PopupMenuSetting())), + contentPadding: EdgeInsets.only(left: 80.0, right: 20), + title: Text('Episode popup menu'), + subtitle: Text('Change the menu when long tap episode'), ), Divider(height: 2), + Container( + height: 30.0, + padding: EdgeInsets.symmetric(horizontal: 80), + alignment: Alignment.centerLeft, + child: Text('Default grid view', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith( + color: Theme.of(context).accentColor)), + ), + ListView( + physics: const BouncingScrollPhysics(), + shrinkWrap: true, + scrollDirection: Axis.vertical, + children: [ + _setDefaultGridView(context, + text: 'Podcast page', key: podcastLayoutKey), + _setDefaultGridView(context, + text: 'Recent tab', key: recentLayoutKey), + _setDefaultGridView(context, + text: 'Favorite tab', key: favLayoutKey), + _setDefaultGridView(context, + text: 'Downlaod tab', key: downloadLayoutKey), + Divider(height: 2), + ]), + Divider(height: 2) ]), ], ), diff --git a/lib/settings/popup_menu.dart b/lib/settings/popup_menu.dart new file mode 100644 index 0000000..00c7e1b --- /dev/null +++ b/lib/settings/popup_menu.dart @@ -0,0 +1,176 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:line_icons/line_icons.dart'; +import 'package:flare_flutter/flare_actor.dart'; + +import '../util/context_extension.dart'; +import '../util/custompaint.dart'; +import '../local_storage/key_value_storage.dart'; + +class PopupMenuSetting extends StatefulWidget { + const PopupMenuSetting({Key key}) : super(key: key); + + @override + _PopupMenuSettingState createState() => _PopupMenuSettingState(); +} + +class _PopupMenuSettingState extends State { + Future> _getEpisodeMenu() async { + KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey); + List list = await popupMenuStorage.getMenu(); + return list; + } + + _saveEpisodeMene(List list) async { + KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey); + await popupMenuStorage.saveMenu(list); + setState(() {}); + } + + Widget _popupMenuItem(List menu, int e, + {Widget icon, + String text, + String description = '', + bool enable = false}) { + return Padding( + key: ObjectKey(text), + padding: EdgeInsets.only(left: 60.0, right: 20), + child: ListTile( + leading: icon, + title: Text(text), + subtitle: Text(description), + trailing: Checkbox( + value: e < 10, + onChanged: e == 0 + ? null + : (bool boo) { + if (boo && e >= 10) { + int index = menu.indexOf(e); + menu.remove(e); + menu.insert(index, e - 10); + _saveEpisodeMene(menu); + } else if (e < 10) { + int index = menu.indexOf(e); + menu.remove(e); + menu.insert(index, e + 10); + _saveEpisodeMene(menu); + } + })), + ); + } + + @override + Widget build(BuildContext context) { + return AnnotatedRegion( + value: SystemUiOverlayStyle( + statusBarIconBrightness: Theme.of(context).accentColorBrightness, + systemNavigationBarColor: context.primaryColor, + systemNavigationBarIconBrightness: + Theme.of(context).accentColorBrightness, + ), + child: Scaffold( + appBar: AppBar( + elevation: 0, + backgroundColor: context.primaryColor, + ), + body: SingleChildScrollView( + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Container( + color: context.primaryColor, + height: 200, + // color: Colors.red, + child: FlareActor( + 'assets/longtap.flr', + alignment: Alignment.center, + animation: 'longtap', + fit: BoxFit.cover, + )), + Divider(height: 2), + Padding( + padding: EdgeInsets.symmetric(vertical: 10), + ), + Container( + height: 30.0, + padding: EdgeInsets.symmetric(horizontal: 80), + alignment: Alignment.centerLeft, + child: Text('Episode popup menu', + style: Theme.of(context) + .textTheme + .bodyText1 + .copyWith(color: Theme.of(context).accentColor)), + ), + FutureBuilder>( + future: _getEpisodeMenu(), + initialData: [0, 1, 12, 13, 14], + builder: (context, snapshot) { + List menu = snapshot.data; + return ListView( + shrinkWrap: true, + children: menu.map((int e) { + int i = e % 10; + switch (i) { + case 0: + return _popupMenuItem(menu, e, + icon: Icon( + LineIcons.play_circle_solid, + color: context.accentColor, + ), + text: 'Play', + description: 'Play the episode'); + break; + case 1: + return _popupMenuItem(menu, e, + icon: Icon( + LineIcons.clock_solid, + color: Colors.cyan, + ), + text: 'Later', + description: 'Add episode to playlist'); + break; + case 2: + return _popupMenuItem(menu, e, + icon: Icon( + LineIcons.heart, + color: Colors.red, + size: 21 + ), + text: 'Like', + description: 'Add episode to favorite'); + break; + case 3: + return _popupMenuItem(menu, e, + icon: SizedBox( + width: 23, + height: 23, + child: CustomPaint( + painter: ListenedAllPainter(Colors.blue, + stroke: 1.5)), + ), + text: 'Mark Listened', + description: 'Mark episode as listened'); + break; + + case 4: + return _popupMenuItem(menu, e, + icon: Icon( + LineIcons.download_solid, + color: Colors.green, + ), + text: 'Download', + description: 'Download episode'); + break; + default: + return Text('Text'); + break; + } + }).toList(), + ); + }), + ], + ), + )), + ); + } +} diff --git a/lib/settings/storage.dart b/lib/settings/storage.dart index 31f8091..7b86c37 100644 --- a/lib/settings/storage.dart +++ b/lib/settings/storage.dart @@ -20,7 +20,7 @@ class _StorageSettingState extends State Animation _animation; _getCacheMax() async { int cache = await cacheStorage.getInt(); - int value = cache == 0 ? 500 : cache ~/ (1024 * 1024); + int value = cache == 0 ? 200 : cache ~/ (1024 * 1024); if (value > 100) { _controller = AnimationController( vsync: this, duration: Duration(milliseconds: value * 2)); @@ -38,6 +38,17 @@ class _StorageSettingState extends State } } + Future _getAutoDownloadNetwork() async { + KeyValueStorage storage = KeyValueStorage(autoDownloadNetworkKey); + int value = await storage.getInt(); + return value != 0; + } + + _setAudtDownloadNetwork(bool boo) async { + KeyValueStorage storage = KeyValueStorage(autoDownloadNetworkKey); + await storage.saveInt(boo ? 1 : 0); + } + double _value; @override @@ -96,29 +107,50 @@ class _StorageSettingState extends State shrinkWrap: true, scrollDirection: Axis.vertical, children: [ - ListTile( - onTap: () => Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DownloadsManage())), - contentPadding: EdgeInsets.only( - left: 80.0, right: 25, bottom: 10, top: 10), - title: Text('Ask before using cellular data'), - subtitle: Text( - 'Ask to confirm when using cellular data to download episodes.'), - trailing: Selector( - selector: (_, settings) => - settings.downloadUsingData, - builder: (_, data, __) { - return Switch( + Selector( + selector: (_, settings) => settings.downloadUsingData, + builder: (_, data, __) { + return ListTile( + onTap: () => settings.downloadUsingData = !data, + contentPadding: EdgeInsets.only( + left: 80.0, right: 25, bottom: 10, top: 10), + title: Text('Ask before using cellular data'), + subtitle: Text( + 'Ask to confirm when using cellular data to download episodes.'), + trailing: Switch( value: data, onChanged: (value) => settings.downloadUsingData = value, - ); - }, - ), + ), + ); + }, ), Divider(height: 2), + FutureBuilder( + future: _getAutoDownloadNetwork(), + initialData: false, + builder: (context, snapshot) { + return ListTile( + onTap: () async { + _setAudtDownloadNetwork(!snapshot.data); + setState(() {}); + }, + contentPadding: EdgeInsets.only( + left: 80.0, right: 25, bottom: 10, top: 10), + title: + Text('Auto download using cellular data'), + subtitle: Text( + 'You can set podcast auto download in group manage page.'), + trailing: Switch( + value: snapshot.data, + onChanged: (value) async { + await _setAudtDownloadNetwork(value); + setState(() {}); + }, + ), + ); + }), + Divider(height: 2), ], ), ]), @@ -151,7 +183,7 @@ class _StorageSettingState extends State builder: (context) => DownloadsManage())), contentPadding: EdgeInsets.symmetric(horizontal: 80.0), title: Text('Downloads'), - subtitle: Text('Manage doanloaded audio files'), + subtitle: Text('Manage downloaded audio files'), ), Divider(height: 2), ListTile( @@ -175,8 +207,10 @@ class _StorageSettingState extends State left: 60.0, right: 20.0, bottom: 10.0), child: SliderTheme( data: Theme.of(context).sliderTheme.copyWith( - showValueIndicator: ShowValueIndicator.always, - ), + showValueIndicator: ShowValueIndicator.always, + trackHeight: 2, + thumbShape: + RoundSliderThumbShape(enabledThumbRadius: 6)), child: Slider( label: '${_value ~/ 100 * 100} Mb', activeColor: context.accentColor, diff --git a/lib/state/audiostate.dart b/lib/state/audiostate.dart index 5114f1f..119bc96 100644 --- a/lib/state/audiostate.dart +++ b/lib/state/audiostate.dart @@ -155,6 +155,8 @@ class AudioPlayerNotifier extends ChangeNotifier { int _currentPosition; double _currentSpeed = 1; BehaviorSubject> queueSubject; + //Update episode card when setting changed + bool _episodeState = false; BasicPlaybackState get audioState => _audioState; @@ -176,6 +178,7 @@ class AudioPlayerNotifier extends ChangeNotifier { int get timeLeft => _timeLeft; double get switchValue => _switchValue; double get currentSpeed => _currentSpeed; + bool get episodeState => _episodeState; set setSwitchValue(double value) { _switchValue = value; @@ -193,6 +196,11 @@ class AudioPlayerNotifier extends ChangeNotifier { notifyListeners(); } + set setEpisodeState(bool boo) { + _episodeState = !_episodeState; + notifyListeners(); + } + Future _getAutoPlay() async { int i = await autoPlayStorage.getInt(); _autoPlay = i == 0 ? true : false; @@ -315,7 +323,6 @@ class AudioPlayerNotifier extends ChangeNotifier { 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) { @@ -506,7 +513,6 @@ class AudioPlayerNotifier extends ChangeNotifier { } sliderSeek(double val) async { - print(val.toString()); if (_audioState != BasicPlaybackState.connecting && _audioState != BasicPlaybackState.none) { _noSlide = false; @@ -633,6 +639,7 @@ class AudioPlayerTask extends BackgroundAudioTask { AudioPlayer _audioPlayer = AudioPlayer(); Completer _completer = Completer(); BasicPlaybackState _skipState; + bool _lostFocus; bool _playing; bool _stopAtEnd; int cacheMax; @@ -662,7 +669,7 @@ class AudioPlayerTask extends BackgroundAudioTask { @override Future onStart() async { _stopAtEnd = false; - + _lostFocus = false; var playerStateSubscription = _audioPlayer.playbackStateStream .where((state) => state == AudioPlaybackState.completed) .listen((state) { @@ -792,9 +799,12 @@ class AudioPlayerTask extends BackgroundAudioTask { @override void onPause() { if (_skipState == null) { - if (_playing == null) {} - _playing = false; - _audioPlayer.pause(); + if (_playing == null) { + } else if (_audioPlayer.playbackEvent.state == + AudioPlaybackState.playing) { + _playing = false; + _audioPlayer.pause(); + } } } @@ -857,32 +867,45 @@ class AudioPlayerTask extends BackgroundAudioTask { milliseconds: AudioServiceBackground.state.position + 30 * 1000)); } + @override + void onRewind() { + _audioPlayer.seek(Duration( + milliseconds: AudioServiceBackground.state.position - 10 * 1000)); + } + @override void onAudioFocusLost() { if (_skipState == null) { - if (_playing == null || - _audioPlayer.playbackState == AudioPlaybackState.none || - _audioPlayer.playbackState == AudioPlaybackState.connecting) {} - _playing = false; - _audioPlayer.pause(); + if (_playing == null) { + } else if (_audioPlayer.playbackEvent.state == + AudioPlaybackState.playing) { + _playing = false; + _lostFocus = true; + _audioPlayer.pause(); + } } } @override void onAudioBecomingNoisy() { if (_skipState == null) { - if (_playing == null) {} - _playing = false; - _audioPlayer.pause(); + if (_playing == null) { + } else if (_audioPlayer.playbackEvent.state == + AudioPlaybackState.playing) { + _playing = false; + _audioPlayer.pause(); + } } } @override void onAudioFocusGained() { if (_skipState == null) { - if (_playing == null) {} - _playing = true; - _audioPlayer.play(); + if (_lostFocus) { + _lostFocus = false; + _playing = true; + _audioPlayer.play(); + } } } diff --git a/lib/state/download_state.dart b/lib/state/download_state.dart index eec88c2..28b6891 100644 --- a/lib/state/download_state.dart +++ b/lib/state/download_state.dart @@ -93,7 +93,8 @@ class DownloadState extends ChangeNotifier { final completeTask = await FlutterDownloader.loadTasksWithRawQuery( query: "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'"); String filePath = 'file://' + - path.join(completeTask.first.savedDir, completeTask.first.filename); + path.join(completeTask.first.savedDir, + Uri.encodeComponent(completeTask.first.filename)); dbHelper.saveMediaId( episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId); EpisodeBrief episode = @@ -124,7 +125,7 @@ class DownloadState extends ChangeNotifier { super.dispose(); } - Future startTask(EpisodeBrief episode) async { + Future startTask(EpisodeBrief episode, {bool showNotification = true}) async { final dir = await getExternalStorageDirectory(); String localPath = path.join(dir.path, episode.feedTitle); final saveDir = Directory(localPath); @@ -137,10 +138,7 @@ class DownloadState extends ChangeNotifier { now.month.toString() + now.day.toString() + now.second.toString(); - String title = episode.title.trim().substring(0, 1) == '#' - ? episode.title.trim().substring(1) - : episode.title.trim(); - String fileName = title + + String fileName = episode.title + datePlus + '.' + episode.enclosureUrl.split('/').last.split('.').last; @@ -148,12 +146,12 @@ class DownloadState extends ChangeNotifier { fileName: fileName, url: episode.enclosureUrl, savedDir: localPath, - showNotification: true, + showNotification: showNotification, openFileFromNotification: false, ); _episodeTasks.add(EpisodeTask(episode, taskId)); var dbHelper = DBHelper(); - await dbHelper.saveDownloaded(taskId, episode.enclosureUrl); + await dbHelper.saveDownloaded(episode.enclosureUrl, taskId); notifyListeners(); } diff --git a/lib/state/refresh_podcast.dart b/lib/state/refresh_podcast.dart index d24ce65..f3d9c91 100644 --- a/lib/state/refresh_podcast.dart +++ b/lib/state/refresh_podcast.dart @@ -2,10 +2,13 @@ import 'dart:isolate'; import 'package:flutter/material.dart'; import 'package:flutter_isolate/flutter_isolate.dart'; -import 'package:tsacdop/local_storage/key_value_storage.dart'; -import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; +import 'package:connectivity/connectivity.dart'; +import '../local_storage/key_value_storage.dart'; +import '../local_storage/sqflite_localpodcast.dart'; import '../type/podcastlocal.dart'; +import '../type/episodebrief.dart'; +import 'download_state.dart'; enum RefreshState { none, fetch, error } @@ -68,13 +71,32 @@ class RefreshWorker extends ChangeNotifier { Future refreshIsolateEntryPoint(SendPort sendPort) async { KeyValueStorage refreshstorage = KeyValueStorage(refreshdateKey); + KeyValueStorage autoDownloadStorage = KeyValueStorage(autoDownloadNetworkKey); await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch); var dbHelper = DBHelper(); List podcastList = await dbHelper.getPodcastLocalAll(); //int i = 0; await Future.forEach(podcastList, (podcastLocal) async { sendPort.send([podcastLocal.title, 1]); - await dbHelper.updatePodcastRss(podcastLocal); + int updateCount = await dbHelper.updatePodcastRss(podcastLocal); + bool autoDownload = await dbHelper.getAutoDownload(podcastLocal.id); + if (autoDownload && updateCount > 0) { + var result = await Connectivity().checkConnectivity(); + int autoDownloadNetwork = await autoDownloadStorage.getInt(); + if (autoDownloadNetwork == 1) { + List episodes = + await dbHelper.getNewEpisodes(podcastLocal.id); + episodes.forEach((episode) { + DownloadState().startTask(episode, showNotification: false); + }); + } else if (result == ConnectivityResult.wifi) { + List episodes = + await dbHelper.getNewEpisodes(podcastLocal.id); + episodes.forEach((episode) { + DownloadState().startTask(episode, showNotification: false); + }); + } + } print('Refresh ' + podcastLocal.title); }); // KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount'); diff --git a/lib/state/settingstate.dart b/lib/state/settingstate.dart index f00cf94..30be1fd 100644 --- a/lib/state/settingstate.dart +++ b/lib/state/settingstate.dart @@ -1,8 +1,8 @@ import 'dart:io'; +import 'package:connectivity/connectivity.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:workmanager/workmanager.dart'; -import 'package:flutter_downloader/flutter_downloader.dart'; import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/key_value_storage.dart'; @@ -24,11 +24,23 @@ void callbackDispatcher() { await dbHelper.updatePodcastRss(podcastLocal, removeMark: lastWork); bool autoDownload = await dbHelper.getAutoDownload(podcastLocal.id); if (autoDownload && updateCount > 0) { - List episodes = - await dbHelper.getNewEpisodes(podcastLocal.id); - episodes.forEach((episode) { - DownloadState().startTask(episode); - }); + var result = await Connectivity().checkConnectivity(); + KeyValueStorage autoDownloadStorage = + KeyValueStorage(autoDownloadNetworkKey); + int autoDownloadNetwork = await autoDownloadStorage.getInt(); + if (autoDownloadNetwork == 1) { + List episodes = + await dbHelper.getNewEpisodes(podcastLocal.id); + episodes.forEach((episode) { + DownloadState().startTask(episode, showNotification: false); + }); + } else if (result == ConnectivityResult.wifi) { + List episodes = + await dbHelper.getNewEpisodes(podcastLocal.id); + episodes.forEach((episode) { + DownloadState().startTask(episode, showNotification: false); + }); + } } print('Refresh ' + podcastLocal.title); }); diff --git a/lib/util/context_extension.dart b/lib/util/context_extension.dart index 11a8eee..ec49fb0 100644 --- a/lib/util/context_extension.dart +++ b/lib/util/context_extension.dart @@ -5,6 +5,7 @@ extension ContextExtension on BuildContext{ Color get accentColor => Theme.of(this).accentColor; Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor; Color get primaryColorDark => Theme.of(this).primaryColorDark; + Color get textColor => Theme.of(this).textTheme.bodyText1.color; Brightness get brightness => Theme.of(this).brightness; double get width => MediaQuery.of(this).size.width; double get height => MediaQuery.of(this).size.width; diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index e94f348..5b58faa 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -11,9 +11,11 @@ import 'package:auto_animated/auto_animated.dart'; import 'open_container.dart'; import '../state/audiostate.dart'; +import '../state/download_state.dart'; import '../type/episodebrief.dart'; import '../episodes/episodedetail.dart'; import '../local_storage/sqflite_localpodcast.dart'; +import '../local_storage/key_value_storage.dart'; import 'colorize.dart'; import 'context_extension.dart'; import 'custompaint.dart'; @@ -51,6 +53,39 @@ class EpisodeGrid extends StatelessWidget { return await dbHelper.isLiked(episode.enclosureUrl); } + Future> _getEpisodeMenu() async { + KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey); + List list = await popupMenuStorage.getMenu(); + return list; + } + + Future _isDownloaded(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + return await dbHelper.isDownloaded(episode.enclosureUrl); + } + + _markListened(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + bool marked = await dbHelper.checkMarked(episode); + if (!marked) { + final PlayHistory history = + PlayHistory(episode.title, episode.enclosureUrl, 0, 1); + await dbHelper.saveHistory(history); + } + } + + Future _saveLiked(String url) async { + var dbHelper = DBHelper(); + int result = await dbHelper.setLiked(url); + return result; + } + + Future _setUnliked(String url) async { + var dbHelper = DBHelper(); + int result = await dbHelper.setUniked(url); + return result; + } + String _stringForSeconds(double seconds) { if (seconds == null) return null; return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; @@ -171,15 +206,23 @@ class EpisodeGrid extends StatelessWidget { color: color, fontStyle: FontStyle.italic), ); + @override Widget build(BuildContext context) { - double _width = MediaQuery.of(context).size.width; + double _width = context.width; Offset _offset; _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, - bool isPlaying, bool isInPlaylist) async { + {bool isPlaying, bool isInPlaylist}) async { + bool isLiked, isDownload; + int isListened; var audio = Provider.of(context, listen: false); + var downloader = Provider.of(context, listen: false); double left = offset.dx; double top = offset.dy; + List menuList = await _getEpisodeMenu(); + if (menuList.contains(3)) isListened = await _isListened(episode); + if (menuList.contains(2)) isLiked = await _isLiked(episode); + if (menuList.contains(4)) isDownload = await _isDownloaded(episode); await showMenu( shape: RoundedRectangleBorder( borderRadius: BorderRadius.all(Radius.circular(10))), @@ -203,39 +246,134 @@ class EpisodeGrid extends StatelessWidget { ], ), ), - PopupMenuItem( - value: 1, - child: Row( - children: [ - Icon( - LineIcons.clock_solid, - color: Colors.red, - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: 2), - ), - !isInPlaylist ? Text('Later') : Text('Remove') - ], - )), + menuList.contains(1) + ? PopupMenuItem( + value: 1, + child: Row( + children: [ + Icon( + LineIcons.clock_solid, + color: Colors.cyan, + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + !isInPlaylist ? Text('Later') : Text('Remove') + ], + )) + : null, + menuList.contains(2) + ? PopupMenuItem( + value: 2, + child: Row( + children: [ + Icon(LineIcons.heart, color: Colors.red, size: 21), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + isLiked + ? Text( + 'Unlike', + ) + : Text('Like') + ], + )) + : null, + menuList.contains(3) + ? PopupMenuItem( + value: 3, + child: Row( + children: [ + SizedBox( + width: 23, + height: 23, + child: CustomPaint( + painter: + ListenedAllPainter(Colors.blue, stroke: 1.5)), + ), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + isListened > 0.95 + ? Text('Listened', + style: TextStyle( + color: context.textColor.withOpacity(0.5))) + : Text('Mark\nListened') + ], + )) + : null, + menuList.contains(4) + ? PopupMenuItem( + value: 4, + child: Row( + children: [ + Icon(LineIcons.download_solid, color: Colors.green), + Padding( + padding: EdgeInsets.symmetric(horizontal: 2), + ), + isDownload + ? Text('Downloaded', + style: TextStyle( + color: context.textColor.withOpacity(0.5))) + : Text('Download') + ], + )) + : null, ], elevation: 5.0, - ).then((value) { - if (value == 0) { - if (!isPlaying) audio.episodeLoad(episode); - } else if (value == 1) { - if (!isInPlaylist) { - audio.addToPlaylist(episode); - Fluttertoast.showToast( - msg: 'Added to playlist', - gravity: ToastGravity.BOTTOM, - ); - } else { - audio.delFromPlaylist(episode); - Fluttertoast.showToast( - msg: 'Removed from playlist', - gravity: ToastGravity.BOTTOM, - ); - } + ).then((value) async { + switch (value) { + case 0: + if (!isPlaying) audio.episodeLoad(episode); + break; + case 1: + if (!isInPlaylist) { + audio.addToPlaylist(episode); + Fluttertoast.showToast( + msg: 'Added to playlist', + gravity: ToastGravity.BOTTOM, + ); + } else { + audio.delFromPlaylist(episode); + Fluttertoast.showToast( + msg: 'Removed from playlist', + gravity: ToastGravity.BOTTOM, + ); + } + break; + case 2: + if (isLiked) { + await _setUnliked(episode.enclosureUrl); + audio.setEpisodeState = true; + Fluttertoast.showToast( + msg: 'Unliked', + gravity: ToastGravity.BOTTOM, + ); + } else { + await _saveLiked(episode.enclosureUrl); + audio.setEpisodeState = true; + Fluttertoast.showToast( + msg: 'Liked', + gravity: ToastGravity.BOTTOM, + ); + } + break; + case 3: + if (isListened < 0.95) { + await _markListened(episode); + audio.setEpisodeState = true; + Fluttertoast.showToast( + msg: 'Mark listened', + gravity: ToastGravity.BOTTOM, + ); + } + break; + + case 4: + if (!isDownload) downloader.startTask(episode); + break; + default: + break; } }); } @@ -273,9 +411,11 @@ class EpisodeGrid extends StatelessWidget { opacity: Tween(begin: index < initNum ? 0 : 1, end: 1) .animate(animation), child: Selector>>( - selector: (_, audio) => Tuple2(audio?.episode, - audio.queue.playlist.map((e) => e.enclosureUrl).toList()), + Tuple3, bool>>( + selector: (_, audio) => Tuple3( + audio?.episode, + audio.queue.playlist.map((e) => e.enclosureUrl).toList(), + audio.episodeState), builder: (_, data, __) => OpenContainerWrapper( episode: episodes[index], closedBuilder: (context, action, boo) => FutureBuilder( @@ -310,12 +450,13 @@ class EpisodeGrid extends StatelessWidget { details.globalPosition.dx, details.globalPosition.dy), onLongPress: () => _showPopupMenu( - _offset, - episodes[index], - context, - data.item1 == episodes[index], - data.item2 - .contains(episodes[index].enclosureUrl)), + _offset, + episodes[index], + context, + isPlaying: data.item1 == episodes[index], + isInPlaylist: data.item2 + .contains(episodes[index].enclosureUrl), + ), onTap: action, child: Container( padding: const EdgeInsets.all(8.0), diff --git a/pubspec.yaml b/pubspec.yaml index ac42d13..0a873ca 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: tsacdop description: An easy-use podacasts player. -version: 0.3.3 +version: 0.3.4 environment: sdk: ">=2.6.0 <3.0.0"