diff --git a/lib/episodes/episodedetail.dart b/lib/episodes/episodedetail.dart index f8b3528..34ecc36 100644 --- a/lib/episodes/episodedetail.dart +++ b/lib/episodes/episodedetail.dart @@ -269,14 +269,15 @@ class _EpisodeDetailState extends State { padding: EdgeInsets.only( left: 20.0, right: 20, bottom: 10), defaultTextStyle: - GoogleFonts.libreBaskerville( + // GoogleFonts.libreBaskerville( + GoogleFonts.martel( textStyle: TextStyle( height: 1.8, ), ), data: _description, linkStyle: TextStyle( - color: Theme.of(context).accentColor, + color: context.accentColor, // decoration: TextDecoration.underline, textBaseline: TextBaseline.ideographic), onLinkTap: (url) { @@ -296,7 +297,7 @@ class _EpisodeDetailState extends State { _launchUrl(link.url); }, text: _description, - style: GoogleFonts.libreBaskerville( + style: GoogleFonts.martel( textStyle: TextStyle( height: 1.8, ), @@ -384,8 +385,6 @@ class MenuBar extends StatefulWidget { } class _MenuBarState extends State { - bool _liked = false; - Future getPosition(EpisodeBrief episode) async { var dbHelper = DBHelper(); return await dbHelper.getPosition(episode); @@ -394,17 +393,14 @@ class _MenuBarState extends State { Future saveLiked(String url) async { var dbHelper = DBHelper(); int result = await dbHelper.setLiked(url); - if (result == 1 && mounted) setState(() => _liked = true); + setState(() {}); return result; } Future setUnliked(String url) async { var dbHelper = DBHelper(); int result = await dbHelper.setUniked(url); - if (result == 1 && mounted) - setState(() { - _liked = false; - }); + setState(() {}); return result; } @@ -418,12 +414,6 @@ class _MenuBarState extends State { return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; } - @override - void initState() { - super.initState(); - _liked = false; - } - Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material( color: Colors.transparent, child: InkWell( @@ -495,7 +485,7 @@ class _MenuBarState extends State { future: _isLiked(widget.episodeItem), initialData: false, builder: (BuildContext context, AsyncSnapshot snapshot) { - return (!snapshot.data && !_liked) + return (!snapshot.data) ? _buttonOnMenu( Icon( Icons.favorite_border, @@ -508,23 +498,13 @@ class _MenuBarState extends State { await Future.delayed(Duration(seconds: 2)); _overlayEntry?.remove(); }) - : (snapshot.data && !_liked) - ? _buttonOnMenu( - Icon( - Icons.favorite, - color: Colors.red, - ), - () => setUnliked( - widget.episodeItem.enclosureUrl)) - : _buttonOnMenu( - Icon( - Icons.favorite, - color: Colors.red, - ), - () { - setUnliked(widget.episodeItem.enclosureUrl); - }, - ); + : _buttonOnMenu( + Icon( + Icons.favorite, + color: Colors.red, + ), + () => + setUnliked(widget.episodeItem.enclosureUrl)); }, ), DownloadButton(episode: widget.episodeItem), @@ -534,7 +514,7 @@ class _MenuBarState extends State { return data.contains(widget.episodeItem) ? _buttonOnMenu( Icon(Icons.playlist_add_check, - color: Theme.of(context).accentColor), () { + color: context.accentColor), () { audio.delFromPlaylist(widget.episodeItem); Fluttertoast.showToast( msg: 'Removed from playlist', diff --git a/lib/home/home_groups.dart b/lib/home/home_groups.dart index 5189888..fabdf45 100644 --- a/lib/home/home_groups.dart +++ b/lib/home/home_groups.dart @@ -376,14 +376,10 @@ class _ScrollPodcastsState extends State { } } -class PodcastPreview extends StatefulWidget { +class PodcastPreview extends StatelessWidget { final PodcastLocal podcastLocal; PodcastPreview({this.podcastLocal, Key key}) : super(key: key); - @override - _PodcastPreviewState createState() => _PodcastPreviewState(); -} -class _PodcastPreviewState extends State { Future> _getRssItemTop(PodcastLocal podcastLocal) async { var dbHelper = DBHelper(); List episodes = await dbHelper.getRssItemTop(podcastLocal.id); @@ -393,8 +389,8 @@ class _PodcastPreviewState extends State { @override Widget build(BuildContext context) { Color _c = (Theme.of(context).brightness == Brightness.light) - ? widget.podcastLocal.primaryColor.colorizedark() - : widget.podcastLocal.primaryColor.colorizeLight(); + ? podcastLocal.primaryColor.colorizedark() + : podcastLocal.primaryColor.colorizeLight(); return Column( children: [ Expanded( @@ -402,7 +398,7 @@ class _PodcastPreviewState extends State { selector: (_, worker) => worker.created, builder: (context, created, child) { return FutureBuilder>( - future: _getRssItemTop(widget.podcastLocal), + future: _getRssItemTop(podcastLocal), builder: (context, snapshot) { if (snapshot.hasError) { print(snapshot.error); @@ -411,7 +407,7 @@ class _PodcastPreviewState extends State { return (snapshot.hasData) ? ShowEpisode( episodes: snapshot.data, - podcastLocal: widget.podcastLocal, + podcastLocal: podcastLocal, ) : Container( padding: EdgeInsets.all(5.0), @@ -429,7 +425,7 @@ class _PodcastPreviewState extends State { children: [ Expanded( flex: 4, - child: Text(widget.podcastLocal.title, + child: Text(podcastLocal.title, maxLines: 1, overflow: TextOverflow.visible, style: TextStyle(fontWeight: FontWeight.bold, color: _c)), @@ -450,11 +446,11 @@ class _PodcastPreviewState extends State { context, SlideLeftHideRoute( transitionPage: PodcastDetail( - podcastLocal: widget.podcastLocal, + podcastLocal: podcastLocal, hide: playerRunning, ), page: PodcastDetail( - podcastLocal: widget.podcastLocal, + podcastLocal: podcastLocal, )), ); }, @@ -606,7 +602,7 @@ class ShowEpisode extends StatelessWidget { Padding( padding: EdgeInsets.symmetric(horizontal: 2), ), - isListened > 0.95 + isListened > 0 ? Text('Listened', style: TextStyle( color: context.textColor.withOpacity(0.5))) @@ -671,7 +667,7 @@ class ShowEpisode extends StatelessWidget { } break; case 3: - if (isListened < 0.95) { + if (isListened < 1) { await _markListened(episode); audio.setEpisodeState = true; Fluttertoast.showToast( diff --git a/lib/local_storage/key_value_storage.dart b/lib/local_storage/key_value_storage.dart index ac1701c..c4eebe9 100644 --- a/lib/local_storage/key_value_storage.dart +++ b/lib/local_storage/key_value_storage.dart @@ -24,6 +24,7 @@ const String favLayoutKey = 'favLayoutKey'; const String downloadLayoutKey = 'downloadLayoutKey'; const String autoDownloadNetworkKey = 'autoDownloadNetwork'; const String episodePopupMenuKey = 'episodePopupMenuKey'; +const String autoDeleteKey = 'autoDeleteKey'; class KeyValueStorage { final String key; diff --git a/lib/local_storage/sqflite_localpodcast.dart b/lib/local_storage/sqflite_localpodcast.dart index f2e1afc..68725c3 100644 --- a/lib/local_storage/sqflite_localpodcast.dart +++ b/lib/local_storage/sqflite_localpodcast.dart @@ -1,3 +1,5 @@ +import 'dart:io'; + import 'package:sqflite/sqflite.dart'; import 'dart:async'; import 'package:path/path.dart'; @@ -272,9 +274,7 @@ class DBHelper { var dbClient = await database; int i = 0; List list = - await dbClient.rawQuery("""SELECT listen_time FROM PlayHistory - WHERE enclosure_url = ? - """, [url]); + await dbClient.rawQuery("SELECT listen_time FROM PlayHistory WHERE enclosure_url = ?", [url]); if (list.length == 0) return 0; else { @@ -708,35 +708,35 @@ class DBHelper { return episodes; } - Future getRssItemDownload(String url) async { - var dbClient = await database; - EpisodeBrief episode; - List list = await dbClient.rawQuery( - """SELECT E.title, E.enclosure_url, E.enclosure_length, - E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked, - E.downloaded, P.primaryColor, E.media_id, E.is_new, P.skip_seconds - FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id - where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""", - [url]); + //Future getRssItemDownload(String url) async { + // var dbClient = await database; + // EpisodeBrief episode; + // List list = await dbClient.rawQuery( + // """SELECT E.title, E.enclosure_url, E.enclosure_length, + // E.milliseconds, P.imagePath, P.title as feed_title, E.duration, E.explicit, E.liked, + // E.downloaded, P.primaryColor, E.media_id, E.is_new, P.skip_seconds + // FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id + // where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""", + // [url]); - if (list != null) - episode = EpisodeBrief( - list.first['title'], - list.first['enclosure_url'], - list.first['enclosure_length'], - list.first['milliseconds'], - list.first['feed_title'], - list.first['primaryColor'], - list.first['liked'], - list.first['downloaded'], - list.first['duration'], - list.first['explicit'], - list.first['imagePath'], - list.first['media_id'], - list.first['is_new'], - list.first['skip_seconds']); - return episode; - } + // if (list != null) + // episode = EpisodeBrief( + // list.first['title'], + // list.first['enclosure_url'], + // list.first['enclosure_length'], + // list.first['milliseconds'], + // list.first['feed_title'], + // list.first['primaryColor'], + // list.first['liked'], + // list.first['downloaded'], + // list.first['duration'], + // list.first['explicit'], + // list.first['imagePath'], + // list.first['media_id'], + // list.first['is_new'], + // list.first['skip_seconds']); + // return episode; + //} Future> getRecentRssItem(int top) async { var dbClient = await database; @@ -835,8 +835,8 @@ class DBHelper { // 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, + // """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 """, @@ -861,6 +861,105 @@ class DBHelper { // } // return episodes; //} + Future> getOutdatedEpisode(int days) async { + var dbClient = await database; + List episodes = []; + if (days > 0) { + int deadline = + DateTime.now().subtract(Duration(days: days)).millisecondsSinceEpoch; + 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 E.download_date < ? AND E.enclosure_url != E.media_id + ORDER BY E.milliseconds DESC""", [deadline]); + 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> getDownloadedEpisode(int mode) async { + var dbClient = await database; + List episodes = []; + List list; + if (mode == 0) + list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, E.download_date, + 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 E.enclosure_url != E.media_id + ORDER BY E.download_date DESC""", + ); + else if (mode == 1) + list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, E.download_date, + 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 E.enclosure_url != E.media_id + ORDER BY E.download_date ASC""", + ); + else if (mode == 2) + list = await dbClient.rawQuery( + """SELECT E.title, E.enclosure_url, E.enclosure_length, E.download_date, + 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 E.enclosure_url != E.media_id + ORDER BY E.enclosure_length DESC""", + ); + + for (int x = 0; x < list.length; x++) { + int size; + if (list[x]['enclosure_length'] == null || + list[x]['enclosure_length'] == 0) { + String uri = list[x]['media_id']; + FileStat fileStat = await File(uri.substring(6)).stat(); + size = fileStat.size; + await dbClient.rawUpdate( + "UPDATE Episodes SET enclosure_length = ?, WHERE media_id = ?", + [size, uri]); + } else + size = list[x]['enclosure_length']; + episodes.add( + EpisodeBrief( + list[x]['title'], + list[x]['enclosure_url'], + size, + 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'], + downloadDate: list[x]['download_date']), + ); + } + return episodes; + } Future removeAllNewMark() async { var dbClient = await database; @@ -995,9 +1094,9 @@ class DBHelper { 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; + 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 { diff --git a/lib/settings/downloads_manage.dart b/lib/settings/downloads_manage.dart index e194d8c..f01f58e 100644 --- a/lib/settings/downloads_manage.dart +++ b/lib/settings/downloads_manage.dart @@ -5,9 +5,12 @@ import 'package:flutter/services.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:path_provider/path_provider.dart'; import 'package:line_icons/line_icons.dart'; +import 'package:provider/provider.dart'; import 'package:google_fonts/google_fonts.dart'; import '../type/episodebrief.dart'; +import '../util/context_extension.dart'; +import '../state/download_state.dart'; import '../local_storage/sqflite_localpodcast.dart'; class DownloadsManage extends StatefulWidget { @@ -18,25 +21,23 @@ class DownloadsManage extends StatefulWidget { class _DownloadsManageState extends State { //Downloaded size int _size; + int _mode; //Downloaded files int _fileNum; - bool _loadEpisodes; bool _clearing; + bool _onlyListened; List _selectedList; - List _episodes = []; - _getDownloadedRssItem() async { - _episodes = []; - final tasks = await FlutterDownloader.loadTasksWithRawQuery( - query: "SELECT * FROM task WHERE status = 3"); + Future> _getDownloadedEpisode(int mode) async { + List episodes = []; var dbHelper = DBHelper(); - await Future.forEach(tasks, (task) async { - EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.url); - _episodes.add(episode); - }); - setState(() { - _loadEpisodes = true; - }); + episodes = await dbHelper.getDownloadedEpisode(mode); + return episodes; + } + + Future _isListened(EpisodeBrief episode) async { + DBHelper dbHelper = DBHelper(); + return await dbHelper.isListened(episode.enclosureUrl); } _getStorageSize() async { @@ -58,12 +59,13 @@ class _DownloadsManageState extends State { _delSelectedEpisodes() async { setState(() => _clearing = true); await Future.forEach(_selectedList, (EpisodeBrief episode) async { - await FlutterDownloader.remove( - taskId: episode.downloaded, shouldDeleteContent: true); - var dbHelper = DBHelper(); - await dbHelper.delDownloaded(episode.enclosureUrl); - setState(() => - _episodes.removeWhere((e) => e.enclosureUrl == episode.enclosureUrl)); + var downloader = Provider.of(context, listen: false); + await downloader.removeTask(episode); + // await FlutterDownloader.remove( + // taskId: episode.downloaded, shouldDeleteContent: true); + // var dbHelper = DBHelper(); + // await dbHelper.delDownloaded(episode.enclosureUrl); + setState(() {}); }); await Future.delayed(Duration(seconds: 1)); setState(() { @@ -90,10 +92,10 @@ class _DownloadsManageState extends State { void initState() { super.initState(); _clearing = false; - _loadEpisodes = false; _selectedList = []; + _mode = 0; + _onlyListened = false; _getStorageSize(); - _getDownloadedRssItem(); } @override @@ -107,9 +109,8 @@ class _DownloadsManageState extends State { ), child: Scaffold( appBar: AppBar( - title: Text('Downloads'), elevation: 0, - backgroundColor: Theme.of(context).primaryColor, + backgroundColor: context.primaryColor, ), body: SafeArea( child: Stack( @@ -119,125 +120,248 @@ class _DownloadsManageState extends State { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.all(10.0), - ), Container( - height: 100.0, - padding: EdgeInsets.only(bottom: 20, left: 60), - alignment: Alignment.centerLeft, - child: RichText( - text: TextSpan( - text: 'Total ', - style: TextStyle( - color: Theme.of(context).accentColor, - fontSize: 20, + height: 140.0, + color: context.primaryColor, + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + children: [ + Padding( + padding: const EdgeInsets.only(left: 20), + child: RichText( + text: TextSpan( + text: 'Total ', + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 20, + ), + children: [ + TextSpan( + text: _fileNum.toString(), + style: GoogleFonts.cairo( + textStyle: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 40, + )), + ), + TextSpan( + text: _fileNum < 2 + ? ' episode' + : ' episodes ', + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 20, + )), + TextSpan( + text: (_size ~/ 1000000).toString(), + style: GoogleFonts.cairo( + textStyle: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 50, + )), + ), + TextSpan( + text: ' Mb', + style: TextStyle( + color: Theme.of(context).accentColor, + fontSize: 20, + )), + ], + ), + ), ), - children: [ - TextSpan( - text: _fileNum.toString(), - style: GoogleFonts.cairo( - textStyle: TextStyle( - color: Theme.of(context).accentColor, - fontSize: 40, - )), - ), - TextSpan( - text: _fileNum < 2 ? ' episode' : ' episodes ', - style: TextStyle( - color: Theme.of(context).accentColor, - fontSize: 20, - )), - TextSpan( - text: (_size ~/ 1000000).toString(), - style: GoogleFonts.cairo( - textStyle: TextStyle( - color: Theme.of(context).accentColor, - fontSize: 50, - )), - ), - TextSpan( - text: ' Mb', - style: TextStyle( - color: Theme.of(context).accentColor, - fontSize: 20, - )), - ], - ), - ), - ), - _loadEpisodes - ? Expanded( - child: ListView.builder( - itemCount: _episodes.length, - shrinkWrap: true, - scrollDirection: Axis.vertical, - itemBuilder: (context, index) { - return Column( - children: [ - ListTile( - onTap: () { - if (_selectedList - .contains(_episodes[index])) { - setState(() => _selectedList - .removeWhere((episode) => - episode.enclosureUrl == - _episodes[index] - .enclosureUrl)); - } else { - setState(() => _selectedList - .add(_episodes[index])); - } - }, - leading: CircleAvatar( - backgroundImage: FileImage(File( - "${_episodes[index].imagePath}")), - ), - title: Text( - _episodes[index].title, - maxLines: 1, - overflow: TextOverflow.ellipsis, - ), - subtitle: _episodes[index] - .enclosureLength != - 0 - ? Text(((_episodes[index] - .enclosureLength) ~/ - 1000000) - .toString() + - ' Mb') - : Center(), - trailing: Checkbox( - value: _selectedList - .contains(_episodes[index]), - onChanged: (bool boo) { - if (boo) { - setState(() => _selectedList - .add(_episodes[index])); - } else { - setState(() => _selectedList - .removeWhere((episode) => - episode.enclosureUrl == - _episodes[index] - .enclosureUrl)); - } - }, - ), + Spacer(), + SizedBox( + height: 40, + 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: 40, + padding: + EdgeInsets.symmetric(horizontal: 20), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Sory by'), + Padding( + padding: EdgeInsets.symmetric( + horizontal: 5), + ), + Icon( + _mode == 0 + ? LineIcons + .hourglass_start_solid + : _mode == 1 + ? LineIcons + .hourglass_half_solid + : LineIcons.save, + size: 18, + ) + ], + )), + itemBuilder: (context) => [ + PopupMenuItem( + value: 0, + child: Text('Newest first'), ), - Divider( - height: 2, + PopupMenuItem( + value: 1, + child: Text('Oldest first'), + ), + PopupMenuItem( + value: 2, + child: Text('Size'), ), ], - ); - }), - ) - : CircularProgressIndicator(), + onSelected: (value) { + if (value == 0) + setState(() => _mode = 0); + else if (value == 1) + setState(() => _mode = 1); + else if (value == 2) + setState(() => _mode = 2); + }, + ), + ), + //Spacer(), + + Material( + color: Colors.transparent, + child: InkWell( + onTap: () => setState(() { + _onlyListened = !_onlyListened; + }), + child: Row( + children: [ + Padding( + padding: + EdgeInsets.symmetric(horizontal: 5), + ), + Text('Listened Only'), + Checkbox( + value: _onlyListened, + onChanged: (value) { + setState(() { + _onlyListened = value; + }); + }), + ], + ), + ), + ), + ], + ), + ), + ], + ), + ), + Expanded( + child: FutureBuilder>( + future: _getDownloadedEpisode(_mode), + initialData: [], + builder: (context, snapshot) { + var _episodes = snapshot.data; + return ListView.builder( + itemCount: _episodes.length, + shrinkWrap: true, + scrollDirection: Axis.vertical, + itemBuilder: (context, index) { + return FutureBuilder( + future: _isListened(_episodes[index]), + initialData: 0, + builder: (context, snapshot) { + return (_onlyListened && + snapshot.data > 0) + ? Center() + : Column( + children: [ + ListTile( + onTap: () { + if (_selectedList.contains( + _episodes[index])) { + setState(() => _selectedList + .removeWhere((episode) => + episode + .enclosureUrl == + _episodes[index] + .enclosureUrl)); + } else { + setState(() => _selectedList + .add(_episodes[index])); + } + }, + leading: CircleAvatar( + backgroundImage: FileImage(File( + "${_episodes[index].imagePath}")), + ), + title: Text( + _episodes[index].title, + maxLines: 1, + overflow: + TextOverflow.ellipsis, + ), + subtitle: Row( + children: [ + Text(_episodes[index] + .downloadDateToString()), + SizedBox(width: 20), + _episodes[index] + .enclosureLength != + 0 + ? Text(((_episodes[index] + .enclosureLength) ~/ + 1000000) + .toString() + + ' Mb') + : SizedBox( + width: 1, + ), + ], + ), + trailing: Checkbox( + value: _selectedList.contains( + _episodes[index]), + onChanged: (bool boo) { + if (boo) { + setState(() => + _selectedList.add( + _episodes[ + index])); + } else { + setState(() => _selectedList + .removeWhere((episode) => + episode + .enclosureUrl == + _episodes[index] + .enclosureUrl)); + } + }, + ), + ), + Divider( + height: 2, + ), + ], + ); + }); + }); + }, + ), + ) ], ), AnimatedPositioned( duration: Duration(milliseconds: 800), curve: Curves.elasticInOut, - left: MediaQuery.of(context).size.width / 2 - 50, + left: context.width / 2 - 50, bottom: _selectedList.length == 0 ? -100 : 30, child: InkWell( onTap: () => _delSelectedEpisodes(), diff --git a/lib/settings/history.dart b/lib/settings/history.dart index 7ea3062..3c7353f 100644 --- a/lib/settings/history.dart +++ b/lib/settings/history.dart @@ -300,14 +300,13 @@ class _PlayedHistoryState extends State builder: (context, snapshot) { return snapshot.hasData ? ListView.builder( - shrinkWrap: true, + // shrinkWrap: true, scrollDirection: Axis.vertical, itemCount: snapshot.data.length, itemBuilder: (BuildContext context, int index) { bool _status = snapshot.data[index].status; return Container( - color: Theme.of(context).scaffoldBackgroundColor, - padding: const EdgeInsets.symmetric(vertical: 5), + color: context.scaffoldBackgroundColor, child: Column( children: [ ListTile( diff --git a/lib/settings/layouts.dart b/lib/settings/layouts.dart index 45d43eb..4686e9c 100644 --- a/lib/settings/layouts.dart +++ b/lib/settings/layouts.dart @@ -180,7 +180,6 @@ class _LayoutSettingState extends State { text: 'Favorite tab', key: favLayoutKey), _setDefaultGridView(context, text: 'Downlaod tab', key: downloadLayoutKey), - Divider(height: 2), ]), Divider(height: 2) ]), diff --git a/lib/settings/licenses.dart b/lib/settings/licenses.dart index ee7c827..200280d 100644 --- a/lib/settings/licenses.dart +++ b/lib/settings/licenses.dart @@ -21,7 +21,8 @@ List google = [ List fonts = [ Libries('Libre Baskerville', font, "https://fonts.google.com/specimen/Libre+Baskerville"), - Libries('Teko', font, "https://fonts.google.com/specimen/Teko") + Libries('Teko', font, "https://fonts.google.com/specimen/Teko"), + Libries('Martel', font, "https://fonts.google.com/specimen/Martel") ]; List plugins = [ @@ -68,5 +69,6 @@ List plugins = [ Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart'), Libries('flutter_isolate', mit, 'https://pub.dev/packages/flutter_isolate'), Libries('auto_animated', mit, 'https://pub.dev/packages/auto_animated'), - Libries('wc_flutter_share', apacheLicense, 'https://pub.dev/packages/wc_flutter_share') + Libries('wc_flutter_share', apacheLicense, + 'https://pub.dev/packages/wc_flutter_share') ]; diff --git a/lib/settings/popup_menu.dart b/lib/settings/popup_menu.dart index 00c7e1b..7a37f88 100644 --- a/lib/settings/popup_menu.dart +++ b/lib/settings/popup_menu.dart @@ -39,6 +39,21 @@ class _PopupMenuSettingState extends State { leading: icon, title: Text(text), subtitle: Text(description), + onTap: e == 0 + ? null + : () { + if (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); + } + }, trailing: Checkbox( value: e < 10, onChanged: e == 0 @@ -131,11 +146,8 @@ class _PopupMenuSettingState extends State { break; case 2: return _popupMenuItem(menu, e, - icon: Icon( - LineIcons.heart, - color: Colors.red, - size: 21 - ), + icon: Icon(LineIcons.heart, + color: Colors.red, size: 21), text: 'Like', description: 'Add episode to favorite'); break; diff --git a/lib/settings/settting.dart b/lib/settings/settting.dart index 0db74a3..1a111e2 100644 --- a/lib/settings/settting.dart +++ b/lib/settings/settting.dart @@ -179,12 +179,6 @@ class _SettingsState extends State leading: Icon(LineIcons.play_circle), title: Text('Play'), subtitle: Text('Playlist and player'), - // trailing: Selector( - // selector: (_, audio) => audio.autoPlay, - // builder: (_, data, __) => Switch( - // value: data, - // onChanged: (boo) => audio.autoPlaySwitch = boo), - // ), ), Divider(height: 2), ListTile( diff --git a/lib/settings/storage.dart b/lib/settings/storage.dart index 7b86c37..b63d6eb 100644 --- a/lib/settings/storage.dart +++ b/lib/settings/storage.dart @@ -30,11 +30,6 @@ class _StorageSettingState extends State setState(() => _value = _animation.value); }); _controller.forward(); - // _controller.addStatusListener((status) { - // if (status == AnimationStatus.completed) { - // _controller.reset(); - // } - // }); } } @@ -44,6 +39,22 @@ class _StorageSettingState extends State return value != 0; } + Future _getAutoDeleteDays() async { + KeyValueStorage storage = KeyValueStorage(autoDeleteKey); + int days = await storage.getInt(); + if (days == 0) { + storage.saveInt(30); + return 30; + } + return days; + } + + _setAutoDeleteDays(int days) async { + KeyValueStorage storage = KeyValueStorage(autoDeleteKey); + await storage.saveInt(days); + setState(() {}); + } + _setAudtDownloadNetwork(bool boo) async { KeyValueStorage storage = KeyValueStorage(autoDownloadNetworkKey); await storage.saveInt(boo ? 1 : 0); @@ -186,10 +197,41 @@ class _StorageSettingState extends State subtitle: Text('Manage downloaded audio files'), ), Divider(height: 2), + FutureBuilder( + future: _getAutoDeleteDays(), + initialData: 30, + builder: (context, snapshot) { + return ListTile( + contentPadding: + EdgeInsets.only(left: 80.0, right: 20), + title: Text('Auto delete downloads after'), + subtitle: Text('Default 30 days.'), + trailing: DropdownButton( + hint: snapshot.data == -1 + ? Text('Never') + : Text(snapshot.data.toString() + 'days'), + underline: Center(), + elevation: 1, + value: snapshot.data, + onChanged: (value) async { + await _setAutoDeleteDays(value); + }, + items: [-1, 10, 30] + .map>((e) { + return DropdownMenuItem( + value: e, + child: e == -1 + ? Text('Never') + : Text(e.toString() + ' days')); + }).toList()), + ); + }, + ), + Divider(height: 2), ListTile( contentPadding: EdgeInsets.only(left: 80.0, right: 25), // leading: Icon(Icons.colorize), - title: Text('Cache'), + title: Text('Audio cache'), subtitle: Text('Audio cache max size'), trailing: Text.rich(TextSpan( text: '${(_value ~/ 100) * 100}', diff --git a/lib/state/download_state.dart b/lib/state/download_state.dart index 28b6891..bf0ebcd 100644 --- a/lib/state/download_state.dart +++ b/lib/state/download_state.dart @@ -4,10 +4,10 @@ import 'dart:ui'; import 'package:flutter/material.dart'; import 'package:flutter/rendering.dart'; -import 'package:intl/intl.dart'; import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:path_provider/path_provider.dart'; import 'package:path/path.dart' as path; +import 'package:tsacdop/local_storage/key_value_storage.dart'; import 'package:tsacdop/local_storage/sqflite_localpodcast.dart'; import '../type/episodebrief.dart'; @@ -37,6 +37,10 @@ class DownloadState extends ChangeNotifier { List _episodeTasks = []; List get episodeTasks => _episodeTasks; + DownloadState() { + _autoDelete(); + } + @override void addListener(VoidCallback listener) async { _loadTasks(); @@ -55,7 +59,6 @@ class DownloadState extends ChangeNotifier { _episodeTasks.add(EpisodeTask(episode, task.taskId, progress: task.progress, status: task.status)); }); - print(_episodeTasks.length); notifyListeners(); } @@ -205,4 +208,19 @@ class DownloadState extends ChangeNotifier { _episodeTasks.removeWhere( (element) => element.episode.enclosureUrl == episode.enclosureUrl); } + + _autoDelete() async { + print('Start auto delete outdated episodes'); + KeyValueStorage autoDeleteStorage = KeyValueStorage(autoDeleteKey); + int autoDelete = await autoDeleteStorage.getInt(); + if (autoDelete == 0) + await autoDeleteStorage.saveInt(30); + else if (autoDelete > 0) { + List episodes = + await dbHelper.getOutdatedEpisode(autoDelete); + if (episodes.length > 0) { + await Future.forEach(episodes, (episode) => delTask(episode)); + } + } + } } diff --git a/lib/type/episodebrief.dart b/lib/type/episodebrief.dart index 9d9780d..f404d02 100644 --- a/lib/type/episodebrief.dart +++ b/lib/type/episodebrief.dart @@ -3,7 +3,7 @@ import 'package:audio_service/audio_service.dart'; class EpisodeBrief { final String title; - String description; + final String description; final int pubDate; final int enclosureLength; final String enclosureUrl; @@ -17,6 +17,7 @@ class EpisodeBrief { final String mediaId; final int isNew; final int skipSeconds; + final int downloadDate; EpisodeBrief( this.title, this.enclosureUrl, @@ -31,7 +32,10 @@ class EpisodeBrief { this.imagePath, this.mediaId, this.isNew, - this.skipSeconds); + this.skipSeconds, + {this.description = '', + this.downloadDate = 0}) + : assert(enclosureUrl != null); String dateToString() { DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true); @@ -50,6 +54,23 @@ class EpisodeBrief { } } + String downloadDateToString() { + DateTime date = DateTime.fromMillisecondsSinceEpoch(downloadDate); + var diffrence = DateTime.now().toUtc().difference(date); + if (diffrence.inHours < 1) { + return '1 hour ago'; + } else if (diffrence.inHours < 24) { + return '${diffrence.inHours} hours ago'; + } else if (diffrence.inHours == 24) { + return '1 day ago'; + } else if (diffrence.inDays < 7) { + return '${diffrence.inDays} days ago'; + } else { + return DateFormat.yMMMd().format( + DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true).toLocal()); + } + } + MediaItem toMediaItem() { return MediaItem( id: mediaId, diff --git a/lib/type/podcastlocal.dart b/lib/type/podcastlocal.dart index 7526474..a37123b 100644 --- a/lib/type/podcastlocal.dart +++ b/lib/type/podcastlocal.dart @@ -3,7 +3,7 @@ class PodcastLocal { final String imageUrl; final String rssUrl; final String author; - + final String primaryColor; final String id; final String imagePath; @@ -13,20 +13,16 @@ class PodcastLocal { final String description; int upateCount; int episodeCount; - PodcastLocal( - this.title, - this.imageUrl, - this.rssUrl, - this.primaryColor, - this.author, - this.id, - this.imagePath, - this.provider, - this.link, - { - this.description ='', - this.upateCount = 0, - this.episodeCount = 0 - } - ); + PodcastLocal(this.title, this.imageUrl, this.rssUrl, this.primaryColor, + this.author, this.id, this.imagePath, this.provider, this.link, + {this.description = '', this.upateCount = 0, this.episodeCount = 0}) + : assert(rssUrl != null); + @override + bool operator ==(Object podcastLocal) => + podcastLocal is PodcastLocal && + podcastLocal.rssUrl == rssUrl && + podcastLocal.id == id; + + @override + int get hashCode => id.hashCode + rssUrl.hashCode; } diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index 5b58faa..d197026 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -118,36 +118,37 @@ class EpisodeGrid extends StatelessWidget { Widget _listenIndicater(BuildContext context, {EpisodeBrief episode, int isListened}) => - Selector>( - selector: (_, audio) => Tuple2(audio.episode, audio.playerRunning), - builder: (_, data, __) { - return (episode.enclosureUrl == data.item1?.enclosureUrl && - data.item2) - ? Container( - height: 20, - width: 20, - margin: EdgeInsets.symmetric(horizontal: 2), - decoration: BoxDecoration( - shape: BoxShape.circle, - ), - child: WaveLoader(color: context.accentColor)) - : layout != Layout.three && isListened > 0 - ? Container( - height: 20, - width: 20, - margin: EdgeInsets.symmetric(horizontal: 2), - padding: EdgeInsets.all(2), - decoration: BoxDecoration( - color: context.accentColor, - shape: BoxShape.circle, - ), - child: CustomPaint( - painter: ListenedAllPainter( - Colors.white, - )), - ) - : Center(); - }); + Center(); + // Selector>( + // selector: (_, audio) => Tuple2(audio.episode, audio.playerRunning), + // builder: (_, data, __) { + // return (episode.enclosureUrl == data.item1?.enclosureUrl && + // data.item2) + // ? Container( + // height: 20, + // width: 20, + // margin: EdgeInsets.symmetric(horizontal: 2), + // decoration: BoxDecoration( + // shape: BoxShape.circle, + // ), + // child: WaveLoader(color: context.accentColor)) + // : layout != Layout.three && isListened > 0 + // ? Container( + // height: 20, + // width: 20, + // margin: EdgeInsets.symmetric(horizontal: 2), + // padding: EdgeInsets.all(2), + // decoration: BoxDecoration( + // color: context.accentColor, + // shape: BoxShape.circle, + // ), + // child: CustomPaint( + // painter: ListenedAllPainter( + // Colors.white, + // )), + // ) + // : Center(); + // }); Widget _downloadIndicater(BuildContext context, {EpisodeBrief episode}) => showDownload || layout != Layout.three @@ -294,7 +295,7 @@ class EpisodeGrid extends StatelessWidget { Padding( padding: EdgeInsets.symmetric(horizontal: 2), ), - isListened > 0.95 + isListened > 0 ? Text('Listened', style: TextStyle( color: context.textColor.withOpacity(0.5))) @@ -359,7 +360,7 @@ class EpisodeGrid extends StatelessWidget { } break; case 3: - if (isListened < 0.95) { + if (isListened < 1) { await _markListened(episode); audio.setEpisodeState = true; Fluttertoast.showToast( @@ -368,7 +369,6 @@ class EpisodeGrid extends StatelessWidget { ); } break; - case 4: if (!isDownload) downloader.startTask(episode); break; @@ -428,7 +428,7 @@ class EpisodeGrid extends StatelessWidget { BorderRadius.all(Radius.circular(5.0)), color: snapshot.data > 0 ? context.brightness == Brightness.light - ? context.primaryColor + ? Colors.grey[200] : Color.fromRGBO(40, 40, 40, 1) : context.scaffoldBackgroundColor, boxShadow: [ @@ -488,9 +488,9 @@ class EpisodeGrid extends StatelessWidget { episode: episodes[index], color: _c), Spacer(), - _listenIndicater(context, - episode: episodes[index], - isListened: snapshot.data), + // _listenIndicater(context, + // episode: episodes[index], + // isListened: snapshot.data), _downloadIndicater(context, episode: episodes[index]), _isNewIndicator(episodes[index]),