Remove listened indicator and increase the color difference

Improve download manage page, support fliters
This commit is contained in:
stonegate 2020-06-11 00:36:53 +08:00
parent 935566b304
commit f4b56938ae
15 changed files with 581 additions and 298 deletions

View File

@ -269,14 +269,15 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
padding: EdgeInsets.only( padding: EdgeInsets.only(
left: 20.0, right: 20, bottom: 10), left: 20.0, right: 20, bottom: 10),
defaultTextStyle: defaultTextStyle:
GoogleFonts.libreBaskerville( // GoogleFonts.libreBaskerville(
GoogleFonts.martel(
textStyle: TextStyle( textStyle: TextStyle(
height: 1.8, height: 1.8,
), ),
), ),
data: _description, data: _description,
linkStyle: TextStyle( linkStyle: TextStyle(
color: Theme.of(context).accentColor, color: context.accentColor,
// decoration: TextDecoration.underline, // decoration: TextDecoration.underline,
textBaseline: TextBaseline.ideographic), textBaseline: TextBaseline.ideographic),
onLinkTap: (url) { onLinkTap: (url) {
@ -296,7 +297,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
_launchUrl(link.url); _launchUrl(link.url);
}, },
text: _description, text: _description,
style: GoogleFonts.libreBaskerville( style: GoogleFonts.martel(
textStyle: TextStyle( textStyle: TextStyle(
height: 1.8, height: 1.8,
), ),
@ -384,8 +385,6 @@ class MenuBar extends StatefulWidget {
} }
class _MenuBarState extends State<MenuBar> { class _MenuBarState extends State<MenuBar> {
bool _liked = false;
Future<PlayHistory> getPosition(EpisodeBrief episode) async { Future<PlayHistory> getPosition(EpisodeBrief episode) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
return await dbHelper.getPosition(episode); return await dbHelper.getPosition(episode);
@ -394,17 +393,14 @@ class _MenuBarState extends State<MenuBar> {
Future<int> saveLiked(String url) async { Future<int> saveLiked(String url) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
int result = await dbHelper.setLiked(url); int result = await dbHelper.setLiked(url);
if (result == 1 && mounted) setState(() => _liked = true); setState(() {});
return result; return result;
} }
Future<int> setUnliked(String url) async { Future<int> setUnliked(String url) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
int result = await dbHelper.setUniked(url); int result = await dbHelper.setUniked(url);
if (result == 1 && mounted) setState(() {});
setState(() {
_liked = false;
});
return result; return result;
} }
@ -418,12 +414,6 @@ class _MenuBarState extends State<MenuBar> {
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
} }
@override
void initState() {
super.initState();
_liked = false;
}
Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material( Widget _buttonOnMenu(Widget widget, VoidCallback onTap) => Material(
color: Colors.transparent, color: Colors.transparent,
child: InkWell( child: InkWell(
@ -495,7 +485,7 @@ class _MenuBarState extends State<MenuBar> {
future: _isLiked(widget.episodeItem), future: _isLiked(widget.episodeItem),
initialData: false, initialData: false,
builder: (BuildContext context, AsyncSnapshot snapshot) { builder: (BuildContext context, AsyncSnapshot snapshot) {
return (!snapshot.data && !_liked) return (!snapshot.data)
? _buttonOnMenu( ? _buttonOnMenu(
Icon( Icon(
Icons.favorite_border, Icons.favorite_border,
@ -508,23 +498,13 @@ class _MenuBarState extends State<MenuBar> {
await Future.delayed(Duration(seconds: 2)); await Future.delayed(Duration(seconds: 2));
_overlayEntry?.remove(); _overlayEntry?.remove();
}) })
: (snapshot.data && !_liked)
? _buttonOnMenu(
Icon(
Icons.favorite,
color: Colors.red,
),
() => setUnliked(
widget.episodeItem.enclosureUrl))
: _buttonOnMenu( : _buttonOnMenu(
Icon( Icon(
Icons.favorite, Icons.favorite,
color: Colors.red, color: Colors.red,
), ),
() { () =>
setUnliked(widget.episodeItem.enclosureUrl); setUnliked(widget.episodeItem.enclosureUrl));
},
);
}, },
), ),
DownloadButton(episode: widget.episodeItem), DownloadButton(episode: widget.episodeItem),
@ -534,7 +514,7 @@ class _MenuBarState extends State<MenuBar> {
return data.contains(widget.episodeItem) return data.contains(widget.episodeItem)
? _buttonOnMenu( ? _buttonOnMenu(
Icon(Icons.playlist_add_check, Icon(Icons.playlist_add_check,
color: Theme.of(context).accentColor), () { color: context.accentColor), () {
audio.delFromPlaylist(widget.episodeItem); audio.delFromPlaylist(widget.episodeItem);
Fluttertoast.showToast( Fluttertoast.showToast(
msg: 'Removed from playlist', msg: 'Removed from playlist',

View File

@ -376,14 +376,10 @@ class _ScrollPodcastsState extends State<ScrollPodcasts> {
} }
} }
class PodcastPreview extends StatefulWidget { class PodcastPreview extends StatelessWidget {
final PodcastLocal podcastLocal; final PodcastLocal podcastLocal;
PodcastPreview({this.podcastLocal, Key key}) : super(key: key); PodcastPreview({this.podcastLocal, Key key}) : super(key: key);
@override
_PodcastPreviewState createState() => _PodcastPreviewState();
}
class _PodcastPreviewState extends State<PodcastPreview> {
Future<List<EpisodeBrief>> _getRssItemTop(PodcastLocal podcastLocal) async { Future<List<EpisodeBrief>> _getRssItemTop(PodcastLocal podcastLocal) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<EpisodeBrief> episodes = await dbHelper.getRssItemTop(podcastLocal.id); List<EpisodeBrief> episodes = await dbHelper.getRssItemTop(podcastLocal.id);
@ -393,8 +389,8 @@ class _PodcastPreviewState extends State<PodcastPreview> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
Color _c = (Theme.of(context).brightness == Brightness.light) Color _c = (Theme.of(context).brightness == Brightness.light)
? widget.podcastLocal.primaryColor.colorizedark() ? podcastLocal.primaryColor.colorizedark()
: widget.podcastLocal.primaryColor.colorizeLight(); : podcastLocal.primaryColor.colorizeLight();
return Column( return Column(
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
@ -402,7 +398,7 @@ class _PodcastPreviewState extends State<PodcastPreview> {
selector: (_, worker) => worker.created, selector: (_, worker) => worker.created,
builder: (context, created, child) { builder: (context, created, child) {
return FutureBuilder<List<EpisodeBrief>>( return FutureBuilder<List<EpisodeBrief>>(
future: _getRssItemTop(widget.podcastLocal), future: _getRssItemTop(podcastLocal),
builder: (context, snapshot) { builder: (context, snapshot) {
if (snapshot.hasError) { if (snapshot.hasError) {
print(snapshot.error); print(snapshot.error);
@ -411,7 +407,7 @@ class _PodcastPreviewState extends State<PodcastPreview> {
return (snapshot.hasData) return (snapshot.hasData)
? ShowEpisode( ? ShowEpisode(
episodes: snapshot.data, episodes: snapshot.data,
podcastLocal: widget.podcastLocal, podcastLocal: podcastLocal,
) )
: Container( : Container(
padding: EdgeInsets.all(5.0), padding: EdgeInsets.all(5.0),
@ -429,7 +425,7 @@ class _PodcastPreviewState extends State<PodcastPreview> {
children: <Widget>[ children: <Widget>[
Expanded( Expanded(
flex: 4, flex: 4,
child: Text(widget.podcastLocal.title, child: Text(podcastLocal.title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.visible, overflow: TextOverflow.visible,
style: TextStyle(fontWeight: FontWeight.bold, color: _c)), style: TextStyle(fontWeight: FontWeight.bold, color: _c)),
@ -450,11 +446,11 @@ class _PodcastPreviewState extends State<PodcastPreview> {
context, context,
SlideLeftHideRoute( SlideLeftHideRoute(
transitionPage: PodcastDetail( transitionPage: PodcastDetail(
podcastLocal: widget.podcastLocal, podcastLocal: podcastLocal,
hide: playerRunning, hide: playerRunning,
), ),
page: PodcastDetail( page: PodcastDetail(
podcastLocal: widget.podcastLocal, podcastLocal: podcastLocal,
)), )),
); );
}, },
@ -606,7 +602,7 @@ class ShowEpisode extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 2), padding: EdgeInsets.symmetric(horizontal: 2),
), ),
isListened > 0.95 isListened > 0
? Text('Listened', ? Text('Listened',
style: TextStyle( style: TextStyle(
color: context.textColor.withOpacity(0.5))) color: context.textColor.withOpacity(0.5)))
@ -671,7 +667,7 @@ class ShowEpisode extends StatelessWidget {
} }
break; break;
case 3: case 3:
if (isListened < 0.95) { if (isListened < 1) {
await _markListened(episode); await _markListened(episode);
audio.setEpisodeState = true; audio.setEpisodeState = true;
Fluttertoast.showToast( Fluttertoast.showToast(

View File

@ -24,6 +24,7 @@ const String favLayoutKey = 'favLayoutKey';
const String downloadLayoutKey = 'downloadLayoutKey'; const String downloadLayoutKey = 'downloadLayoutKey';
const String autoDownloadNetworkKey = 'autoDownloadNetwork'; const String autoDownloadNetworkKey = 'autoDownloadNetwork';
const String episodePopupMenuKey = 'episodePopupMenuKey'; const String episodePopupMenuKey = 'episodePopupMenuKey';
const String autoDeleteKey = 'autoDeleteKey';
class KeyValueStorage { class KeyValueStorage {
final String key; final String key;

View File

@ -1,3 +1,5 @@
import 'dart:io';
import 'package:sqflite/sqflite.dart'; import 'package:sqflite/sqflite.dart';
import 'dart:async'; import 'dart:async';
import 'package:path/path.dart'; import 'package:path/path.dart';
@ -272,9 +274,7 @@ class DBHelper {
var dbClient = await database; var dbClient = await database;
int i = 0; int i = 0;
List<Map> list = List<Map> list =
await dbClient.rawQuery("""SELECT listen_time FROM PlayHistory await dbClient.rawQuery("SELECT listen_time FROM PlayHistory WHERE enclosure_url = ?", [url]);
WHERE enclosure_url = ?
""", [url]);
if (list.length == 0) if (list.length == 0)
return 0; return 0;
else { else {
@ -708,35 +708,35 @@ class DBHelper {
return episodes; return episodes;
} }
Future<EpisodeBrief> getRssItemDownload(String url) async { //Future<EpisodeBrief> getRssItemDownload(String url) async {
var dbClient = await database; // var dbClient = await database;
EpisodeBrief episode; // EpisodeBrief episode;
List<Map> list = await dbClient.rawQuery( // List<Map> list = await dbClient.rawQuery(
"""SELECT E.title, E.enclosure_url, E.enclosure_length, // """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.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 // 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 // FROM Episodes E INNER JOIN PodcastLocal P ON E.feed_id = P.id
where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""", // where E.enclosure_url = ? ORDER BY E.milliseconds DESC LIMIT 3""",
[url]); // [url]);
if (list != null) // if (list != null)
episode = EpisodeBrief( // episode = EpisodeBrief(
list.first['title'], // list.first['title'],
list.first['enclosure_url'], // list.first['enclosure_url'],
list.first['enclosure_length'], // list.first['enclosure_length'],
list.first['milliseconds'], // list.first['milliseconds'],
list.first['feed_title'], // list.first['feed_title'],
list.first['primaryColor'], // list.first['primaryColor'],
list.first['liked'], // list.first['liked'],
list.first['downloaded'], // list.first['downloaded'],
list.first['duration'], // list.first['duration'],
list.first['explicit'], // list.first['explicit'],
list.first['imagePath'], // list.first['imagePath'],
list.first['media_id'], // list.first['media_id'],
list.first['is_new'], // list.first['is_new'],
list.first['skip_seconds']); // list.first['skip_seconds']);
return episode; // return episode;
} //}
Future<List<EpisodeBrief>> getRecentRssItem(int top) async { Future<List<EpisodeBrief>> getRecentRssItem(int top) async {
var dbClient = await database; var dbClient = await database;
@ -861,6 +861,105 @@ class DBHelper {
// } // }
// return episodes; // return episodes;
//} //}
Future<List<EpisodeBrief>> getOutdatedEpisode(int days) async {
var dbClient = await database;
List<EpisodeBrief> episodes = [];
if (days > 0) {
int deadline =
DateTime.now().subtract(Duration(days: days)).millisecondsSinceEpoch;
List<Map> 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<List<EpisodeBrief>> getDownloadedEpisode(int mode) async {
var dbClient = await database;
List<EpisodeBrief> episodes = [];
List<Map> 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<int> removeAllNewMark() async { Future<int> removeAllNewMark() async {
var dbClient = await database; var dbClient = await database;
@ -995,8 +1094,8 @@ class DBHelper {
Future<bool> isDownloaded(String url) async { Future<bool> isDownloaded(String url) async {
var dbClient = await database; var dbClient = await database;
List<Map> list = await dbClient List<Map> list = await dbClient.rawQuery(
.rawQuery("SELECT downloaded FROM Episodes WHERE enclosure_url = ?", [url]); "SELECT downloaded FROM Episodes WHERE enclosure_url = ?", [url]);
return list.first['downloaded'] == 'ND' ? false : true; return list.first['downloaded'] == 'ND' ? false : true;
} }

View File

@ -5,9 +5,12 @@ import 'package:flutter/services.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:line_icons/line_icons.dart'; import 'package:line_icons/line_icons.dart';
import 'package:provider/provider.dart';
import 'package:google_fonts/google_fonts.dart'; import 'package:google_fonts/google_fonts.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
import '../util/context_extension.dart';
import '../state/download_state.dart';
import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/sqflite_localpodcast.dart';
class DownloadsManage extends StatefulWidget { class DownloadsManage extends StatefulWidget {
@ -18,25 +21,23 @@ class DownloadsManage extends StatefulWidget {
class _DownloadsManageState extends State<DownloadsManage> { class _DownloadsManageState extends State<DownloadsManage> {
//Downloaded size //Downloaded size
int _size; int _size;
int _mode;
//Downloaded files //Downloaded files
int _fileNum; int _fileNum;
bool _loadEpisodes;
bool _clearing; bool _clearing;
bool _onlyListened;
List<EpisodeBrief> _selectedList; List<EpisodeBrief> _selectedList;
List<EpisodeBrief> _episodes = [];
_getDownloadedRssItem() async { Future<List<EpisodeBrief>> _getDownloadedEpisode(int mode) async {
_episodes = []; List<EpisodeBrief> episodes = [];
final tasks = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE status = 3");
var dbHelper = DBHelper(); var dbHelper = DBHelper();
await Future.forEach(tasks, (task) async { episodes = await dbHelper.getDownloadedEpisode(mode);
EpisodeBrief episode = await dbHelper.getRssItemWithUrl(task.url); return episodes;
_episodes.add(episode); }
});
setState(() { Future<int> _isListened(EpisodeBrief episode) async {
_loadEpisodes = true; DBHelper dbHelper = DBHelper();
}); return await dbHelper.isListened(episode.enclosureUrl);
} }
_getStorageSize() async { _getStorageSize() async {
@ -58,12 +59,13 @@ class _DownloadsManageState extends State<DownloadsManage> {
_delSelectedEpisodes() async { _delSelectedEpisodes() async {
setState(() => _clearing = true); setState(() => _clearing = true);
await Future.forEach(_selectedList, (EpisodeBrief episode) async { await Future.forEach(_selectedList, (EpisodeBrief episode) async {
await FlutterDownloader.remove( var downloader = Provider.of<DownloadState>(context, listen: false);
taskId: episode.downloaded, shouldDeleteContent: true); await downloader.removeTask(episode);
var dbHelper = DBHelper(); // await FlutterDownloader.remove(
await dbHelper.delDownloaded(episode.enclosureUrl); // taskId: episode.downloaded, shouldDeleteContent: true);
setState(() => // var dbHelper = DBHelper();
_episodes.removeWhere((e) => e.enclosureUrl == episode.enclosureUrl)); // await dbHelper.delDownloaded(episode.enclosureUrl);
setState(() {});
}); });
await Future.delayed(Duration(seconds: 1)); await Future.delayed(Duration(seconds: 1));
setState(() { setState(() {
@ -90,10 +92,10 @@ class _DownloadsManageState extends State<DownloadsManage> {
void initState() { void initState() {
super.initState(); super.initState();
_clearing = false; _clearing = false;
_loadEpisodes = false;
_selectedList = []; _selectedList = [];
_mode = 0;
_onlyListened = false;
_getStorageSize(); _getStorageSize();
_getDownloadedRssItem();
} }
@override @override
@ -107,9 +109,8 @@ class _DownloadsManageState extends State<DownloadsManage> {
), ),
child: Scaffold( child: Scaffold(
appBar: AppBar( appBar: AppBar(
title: Text('Downloads'),
elevation: 0, elevation: 0,
backgroundColor: Theme.of(context).primaryColor, backgroundColor: context.primaryColor,
), ),
body: SafeArea( body: SafeArea(
child: Stack( child: Stack(
@ -119,13 +120,14 @@ class _DownloadsManageState extends State<DownloadsManage> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[ children: <Widget>[
Padding(
padding: EdgeInsets.all(10.0),
),
Container( Container(
height: 100.0, height: 140.0,
padding: EdgeInsets.only(bottom: 20, left: 60), color: context.primaryColor,
alignment: Alignment.centerLeft, child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Padding(
padding: const EdgeInsets.only(left: 20),
child: RichText( child: RichText(
text: TextSpan( text: TextSpan(
text: 'Total ', text: 'Total ',
@ -143,7 +145,9 @@ class _DownloadsManageState extends State<DownloadsManage> {
)), )),
), ),
TextSpan( TextSpan(
text: _fileNum < 2 ? ' episode' : ' episodes ', text: _fileNum < 2
? ' episode'
: ' episodes ',
style: TextStyle( style: TextStyle(
color: Theme.of(context).accentColor, color: Theme.of(context).accentColor,
fontSize: 20, fontSize: 20,
@ -166,22 +170,127 @@ class _DownloadsManageState extends State<DownloadsManage> {
), ),
), ),
), ),
_loadEpisodes Spacer(),
? Expanded( SizedBox(
child: ListView.builder( height: 40,
child: Row(
children: [
Material(
color: Colors.transparent,
child: PopupMenuButton<int>(
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: <Widget>[
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'),
),
PopupMenuItem(
value: 1,
child: Text('Oldest first'),
),
PopupMenuItem(
value: 2,
child: Text('Size'),
),
],
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<List<EpisodeBrief>>(
future: _getDownloadedEpisode(_mode),
initialData: [],
builder: (context, snapshot) {
var _episodes = snapshot.data;
return ListView.builder(
itemCount: _episodes.length, itemCount: _episodes.length,
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
itemBuilder: (context, index) { itemBuilder: (context, index) {
return Column( return FutureBuilder(
future: _isListened(_episodes[index]),
initialData: 0,
builder: (context, snapshot) {
return (_onlyListened &&
snapshot.data > 0)
? Center()
: Column(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
onTap: () { onTap: () {
if (_selectedList if (_selectedList.contains(
.contains(_episodes[index])) { _episodes[index])) {
setState(() => _selectedList setState(() => _selectedList
.removeWhere((episode) => .removeWhere((episode) =>
episode.enclosureUrl == episode
.enclosureUrl ==
_episodes[index] _episodes[index]
.enclosureUrl)); .enclosureUrl));
} else { } else {
@ -196,9 +305,15 @@ class _DownloadsManageState extends State<DownloadsManage> {
title: Text( title: Text(
_episodes[index].title, _episodes[index].title,
maxLines: 1, maxLines: 1,
overflow: TextOverflow.ellipsis, overflow:
TextOverflow.ellipsis,
), ),
subtitle: _episodes[index] subtitle: Row(
children: [
Text(_episodes[index]
.downloadDateToString()),
SizedBox(width: 20),
_episodes[index]
.enclosureLength != .enclosureLength !=
0 0
? Text(((_episodes[index] ? Text(((_episodes[index]
@ -206,18 +321,25 @@ class _DownloadsManageState extends State<DownloadsManage> {
1000000) 1000000)
.toString() + .toString() +
' Mb') ' Mb')
: Center(), : SizedBox(
width: 1,
),
],
),
trailing: Checkbox( trailing: Checkbox(
value: _selectedList value: _selectedList.contains(
.contains(_episodes[index]), _episodes[index]),
onChanged: (bool boo) { onChanged: (bool boo) {
if (boo) { if (boo) {
setState(() => _selectedList setState(() =>
.add(_episodes[index])); _selectedList.add(
_episodes[
index]));
} else { } else {
setState(() => _selectedList setState(() => _selectedList
.removeWhere((episode) => .removeWhere((episode) =>
episode.enclosureUrl == episode
.enclosureUrl ==
_episodes[index] _episodes[index]
.enclosureUrl)); .enclosureUrl));
} }
@ -229,15 +351,17 @@ class _DownloadsManageState extends State<DownloadsManage> {
), ),
], ],
); );
}), });
});
},
),
) )
: CircularProgressIndicator(),
], ],
), ),
AnimatedPositioned( AnimatedPositioned(
duration: Duration(milliseconds: 800), duration: Duration(milliseconds: 800),
curve: Curves.elasticInOut, curve: Curves.elasticInOut,
left: MediaQuery.of(context).size.width / 2 - 50, left: context.width / 2 - 50,
bottom: _selectedList.length == 0 ? -100 : 30, bottom: _selectedList.length == 0 ? -100 : 30,
child: InkWell( child: InkWell(
onTap: () => _delSelectedEpisodes(), onTap: () => _delSelectedEpisodes(),

View File

@ -300,14 +300,13 @@ class _PlayedHistoryState extends State<PlayedHistory>
builder: (context, snapshot) { builder: (context, snapshot) {
return snapshot.hasData return snapshot.hasData
? ListView.builder( ? ListView.builder(
shrinkWrap: true, // shrinkWrap: true,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
itemCount: snapshot.data.length, itemCount: snapshot.data.length,
itemBuilder: (BuildContext context, int index) { itemBuilder: (BuildContext context, int index) {
bool _status = snapshot.data[index].status; bool _status = snapshot.data[index].status;
return Container( return Container(
color: Theme.of(context).scaffoldBackgroundColor, color: context.scaffoldBackgroundColor,
padding: const EdgeInsets.symmetric(vertical: 5),
child: Column( child: Column(
children: <Widget>[ children: <Widget>[
ListTile( ListTile(

View File

@ -180,7 +180,6 @@ class _LayoutSettingState extends State<LayoutSetting> {
text: 'Favorite tab', key: favLayoutKey), text: 'Favorite tab', key: favLayoutKey),
_setDefaultGridView(context, _setDefaultGridView(context,
text: 'Downlaod tab', key: downloadLayoutKey), text: 'Downlaod tab', key: downloadLayoutKey),
Divider(height: 2),
]), ]),
Divider(height: 2) Divider(height: 2)
]), ]),

View File

@ -21,7 +21,8 @@ List<Libries> google = [
List<Libries> fonts = [ List<Libries> fonts = [
Libries('Libre Baskerville', font, Libries('Libre Baskerville', font,
"https://fonts.google.com/specimen/Libre+Baskerville"), "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<Libries> plugins = [ List<Libries> plugins = [
@ -68,5 +69,6 @@ List<Libries> plugins = [
Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart'), Libries('Rxdart', apacheLicense, 'https://pub.dev/packages/rxdart'),
Libries('flutter_isolate', mit, 'https://pub.dev/packages/flutter_isolate'), Libries('flutter_isolate', mit, 'https://pub.dev/packages/flutter_isolate'),
Libries('auto_animated', mit, 'https://pub.dev/packages/auto_animated'), 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')
]; ];

View File

@ -39,6 +39,21 @@ class _PopupMenuSettingState extends State<PopupMenuSetting> {
leading: icon, leading: icon,
title: Text(text), title: Text(text),
subtitle: Text(description), 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( trailing: Checkbox(
value: e < 10, value: e < 10,
onChanged: e == 0 onChanged: e == 0
@ -131,11 +146,8 @@ class _PopupMenuSettingState extends State<PopupMenuSetting> {
break; break;
case 2: case 2:
return _popupMenuItem(menu, e, return _popupMenuItem(menu, e,
icon: Icon( icon: Icon(LineIcons.heart,
LineIcons.heart, color: Colors.red, size: 21),
color: Colors.red,
size: 21
),
text: 'Like', text: 'Like',
description: 'Add episode to favorite'); description: 'Add episode to favorite');
break; break;

View File

@ -179,12 +179,6 @@ class _SettingsState extends State<Settings>
leading: Icon(LineIcons.play_circle), leading: Icon(LineIcons.play_circle),
title: Text('Play'), title: Text('Play'),
subtitle: Text('Playlist and player'), subtitle: Text('Playlist and player'),
// trailing: Selector<AudioPlayerNotifier, bool>(
// selector: (_, audio) => audio.autoPlay,
// builder: (_, data, __) => Switch(
// value: data,
// onChanged: (boo) => audio.autoPlaySwitch = boo),
// ),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(

View File

@ -30,11 +30,6 @@ class _StorageSettingState extends State<StorageSetting>
setState(() => _value = _animation.value); setState(() => _value = _animation.value);
}); });
_controller.forward(); _controller.forward();
// _controller.addStatusListener((status) {
// if (status == AnimationStatus.completed) {
// _controller.reset();
// }
// });
} }
} }
@ -44,6 +39,22 @@ class _StorageSettingState extends State<StorageSetting>
return value != 0; return value != 0;
} }
Future<int> _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 { _setAudtDownloadNetwork(bool boo) async {
KeyValueStorage storage = KeyValueStorage(autoDownloadNetworkKey); KeyValueStorage storage = KeyValueStorage(autoDownloadNetworkKey);
await storage.saveInt(boo ? 1 : 0); await storage.saveInt(boo ? 1 : 0);
@ -186,10 +197,41 @@ class _StorageSettingState extends State<StorageSetting>
subtitle: Text('Manage downloaded audio files'), subtitle: Text('Manage downloaded audio files'),
), ),
Divider(height: 2), Divider(height: 2),
FutureBuilder<int>(
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: <int>[-1, 10, 30]
.map<DropdownMenuItem<int>>((e) {
return DropdownMenuItem<int>(
value: e,
child: e == -1
? Text('Never')
: Text(e.toString() + ' days'));
}).toList()),
);
},
),
Divider(height: 2),
ListTile( ListTile(
contentPadding: EdgeInsets.only(left: 80.0, right: 25), contentPadding: EdgeInsets.only(left: 80.0, right: 25),
// leading: Icon(Icons.colorize), // leading: Icon(Icons.colorize),
title: Text('Cache'), title: Text('Audio cache'),
subtitle: Text('Audio cache max size'), subtitle: Text('Audio cache max size'),
trailing: Text.rich(TextSpan( trailing: Text.rich(TextSpan(
text: '${(_value ~/ 100) * 100}', text: '${(_value ~/ 100) * 100}',

View File

@ -4,10 +4,10 @@ import 'dart:ui';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/rendering.dart'; import 'package:flutter/rendering.dart';
import 'package:intl/intl.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:path/path.dart' as path; 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 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
@ -37,6 +37,10 @@ class DownloadState extends ChangeNotifier {
List<EpisodeTask> _episodeTasks = []; List<EpisodeTask> _episodeTasks = [];
List<EpisodeTask> get episodeTasks => _episodeTasks; List<EpisodeTask> get episodeTasks => _episodeTasks;
DownloadState() {
_autoDelete();
}
@override @override
void addListener(VoidCallback listener) async { void addListener(VoidCallback listener) async {
_loadTasks(); _loadTasks();
@ -55,7 +59,6 @@ class DownloadState extends ChangeNotifier {
_episodeTasks.add(EpisodeTask(episode, task.taskId, _episodeTasks.add(EpisodeTask(episode, task.taskId,
progress: task.progress, status: task.status)); progress: task.progress, status: task.status));
}); });
print(_episodeTasks.length);
notifyListeners(); notifyListeners();
} }
@ -205,4 +208,19 @@ class DownloadState extends ChangeNotifier {
_episodeTasks.removeWhere( _episodeTasks.removeWhere(
(element) => element.episode.enclosureUrl == episode.enclosureUrl); (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<EpisodeBrief> episodes =
await dbHelper.getOutdatedEpisode(autoDelete);
if (episodes.length > 0) {
await Future.forEach(episodes, (episode) => delTask(episode));
}
}
}
} }

View File

@ -3,7 +3,7 @@ import 'package:audio_service/audio_service.dart';
class EpisodeBrief { class EpisodeBrief {
final String title; final String title;
String description; final String description;
final int pubDate; final int pubDate;
final int enclosureLength; final int enclosureLength;
final String enclosureUrl; final String enclosureUrl;
@ -17,6 +17,7 @@ class EpisodeBrief {
final String mediaId; final String mediaId;
final int isNew; final int isNew;
final int skipSeconds; final int skipSeconds;
final int downloadDate;
EpisodeBrief( EpisodeBrief(
this.title, this.title,
this.enclosureUrl, this.enclosureUrl,
@ -31,7 +32,10 @@ class EpisodeBrief {
this.imagePath, this.imagePath,
this.mediaId, this.mediaId,
this.isNew, this.isNew,
this.skipSeconds); this.skipSeconds,
{this.description = '',
this.downloadDate = 0})
: assert(enclosureUrl != null);
String dateToString() { String dateToString() {
DateTime date = DateTime.fromMillisecondsSinceEpoch(pubDate, isUtc: true); 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() { MediaItem toMediaItem() {
return MediaItem( return MediaItem(
id: mediaId, id: mediaId,

View File

@ -13,20 +13,16 @@ class PodcastLocal {
final String description; final String description;
int upateCount; int upateCount;
int episodeCount; int episodeCount;
PodcastLocal( PodcastLocal(this.title, this.imageUrl, this.rssUrl, this.primaryColor,
this.title, this.author, this.id, this.imagePath, this.provider, this.link,
this.imageUrl, {this.description = '', this.upateCount = 0, this.episodeCount = 0})
this.rssUrl, : assert(rssUrl != null);
this.primaryColor, @override
this.author, bool operator ==(Object podcastLocal) =>
this.id, podcastLocal is PodcastLocal &&
this.imagePath, podcastLocal.rssUrl == rssUrl &&
this.provider, podcastLocal.id == id;
this.link,
{ @override
this.description ='', int get hashCode => id.hashCode + rssUrl.hashCode;
this.upateCount = 0,
this.episodeCount = 0
}
);
} }

View File

@ -118,36 +118,37 @@ class EpisodeGrid extends StatelessWidget {
Widget _listenIndicater(BuildContext context, Widget _listenIndicater(BuildContext context,
{EpisodeBrief episode, int isListened}) => {EpisodeBrief episode, int isListened}) =>
Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, bool>>( Center();
selector: (_, audio) => Tuple2(audio.episode, audio.playerRunning), // Selector<AudioPlayerNotifier, Tuple2<EpisodeBrief, bool>>(
builder: (_, data, __) { // selector: (_, audio) => Tuple2(audio.episode, audio.playerRunning),
return (episode.enclosureUrl == data.item1?.enclosureUrl && // builder: (_, data, __) {
data.item2) // return (episode.enclosureUrl == data.item1?.enclosureUrl &&
? Container( // data.item2)
height: 20, // ? Container(
width: 20, // height: 20,
margin: EdgeInsets.symmetric(horizontal: 2), // width: 20,
decoration: BoxDecoration( // margin: EdgeInsets.symmetric(horizontal: 2),
shape: BoxShape.circle, // decoration: BoxDecoration(
), // shape: BoxShape.circle,
child: WaveLoader(color: context.accentColor)) // ),
: layout != Layout.three && isListened > 0 // child: WaveLoader(color: context.accentColor))
? Container( // : layout != Layout.three && isListened > 0
height: 20, // ? Container(
width: 20, // height: 20,
margin: EdgeInsets.symmetric(horizontal: 2), // width: 20,
padding: EdgeInsets.all(2), // margin: EdgeInsets.symmetric(horizontal: 2),
decoration: BoxDecoration( // padding: EdgeInsets.all(2),
color: context.accentColor, // decoration: BoxDecoration(
shape: BoxShape.circle, // color: context.accentColor,
), // shape: BoxShape.circle,
child: CustomPaint( // ),
painter: ListenedAllPainter( // child: CustomPaint(
Colors.white, // painter: ListenedAllPainter(
)), // Colors.white,
) // )),
: Center(); // )
}); // : Center();
// });
Widget _downloadIndicater(BuildContext context, {EpisodeBrief episode}) => Widget _downloadIndicater(BuildContext context, {EpisodeBrief episode}) =>
showDownload || layout != Layout.three showDownload || layout != Layout.three
@ -294,7 +295,7 @@ class EpisodeGrid extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.symmetric(horizontal: 2), padding: EdgeInsets.symmetric(horizontal: 2),
), ),
isListened > 0.95 isListened > 0
? Text('Listened', ? Text('Listened',
style: TextStyle( style: TextStyle(
color: context.textColor.withOpacity(0.5))) color: context.textColor.withOpacity(0.5)))
@ -359,7 +360,7 @@ class EpisodeGrid extends StatelessWidget {
} }
break; break;
case 3: case 3:
if (isListened < 0.95) { if (isListened < 1) {
await _markListened(episode); await _markListened(episode);
audio.setEpisodeState = true; audio.setEpisodeState = true;
Fluttertoast.showToast( Fluttertoast.showToast(
@ -368,7 +369,6 @@ class EpisodeGrid extends StatelessWidget {
); );
} }
break; break;
case 4: case 4:
if (!isDownload) downloader.startTask(episode); if (!isDownload) downloader.startTask(episode);
break; break;
@ -428,7 +428,7 @@ class EpisodeGrid extends StatelessWidget {
BorderRadius.all(Radius.circular(5.0)), BorderRadius.all(Radius.circular(5.0)),
color: snapshot.data > 0 color: snapshot.data > 0
? context.brightness == Brightness.light ? context.brightness == Brightness.light
? context.primaryColor ? Colors.grey[200]
: Color.fromRGBO(40, 40, 40, 1) : Color.fromRGBO(40, 40, 40, 1)
: context.scaffoldBackgroundColor, : context.scaffoldBackgroundColor,
boxShadow: [ boxShadow: [
@ -488,9 +488,9 @@ class EpisodeGrid extends StatelessWidget {
episode: episodes[index], episode: episodes[index],
color: _c), color: _c),
Spacer(), Spacer(),
_listenIndicater(context, // _listenIndicater(context,
episode: episodes[index], // episode: episodes[index],
isListened: snapshot.data), // isListened: snapshot.data),
_downloadIndicater(context, _downloadIndicater(context,
episode: episodes[index]), episode: episodes[index]),
_isNewIndicator(episodes[index]), _isNewIndicator(episodes[index]),