Popup menu setting

Auto download on work
Add rewind when using headset
Fixed audio auto play when notification come
This commit is contained in:
stonegate 2020-06-10 15:42:40 +08:00
parent 1a497a78ed
commit 935566b304
24 changed files with 1082 additions and 432 deletions

View File

@ -49,7 +49,7 @@ android {
applicationId "com.stonegate.tsacdop" applicationId "com.stonegate.tsacdop"
minSdkVersion 19 minSdkVersion 19
targetSdkVersion 29 targetSdkVersion 29
versionCode 16 versionCode 17
versionName flutterVersionName versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
} }

View File

@ -55,10 +55,12 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
setState(() { setState(() {
_showMenu = true; _showMenu = true;
}); });
} else } else if (_controller.offset <
_controller.position.maxScrollExtent * 0.8) {
setState(() { setState(() {
_showMenu = false; _showMenu = false;
}); });
}
} }
_launchUrl(String url) async { _launchUrl(String url) async {
@ -113,7 +115,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
PopupMenuButton( PopupMenuButton(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(Radius.circular(10))),
elevation: 2, elevation: 1,
tooltip: 'Menu', tooltip: 'Menu',
itemBuilder: (context) => [ itemBuilder: (context) => [
PopupMenuItem( PopupMenuItem(
@ -152,6 +154,8 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
gravity: ToastGravity.BOTTOM, gravity: ToastGravity.BOTTOM,
); );
break; break;
default:
break;
} }
}, },
), ),
@ -323,8 +327,7 @@ class _EpisodeDetailState extends State<EpisodeDetail> {
'Still no shownote received\n for this episode.', 'Still no shownote received\n for this episode.',
textAlign: TextAlign.center, textAlign: TextAlign.center,
style: TextStyle( style: TextStyle(
color: context.textTheme color: context.textColor
.bodyText1.color
.withOpacity(0.5))), .withOpacity(0.5))),
], ],
), ),
@ -381,7 +384,7 @@ class MenuBar extends StatefulWidget {
} }
class _MenuBarState extends State<MenuBar> { class _MenuBarState extends State<MenuBar> {
bool _liked; bool _liked = false;
Future<PlayHistory> getPosition(EpisodeBrief episode) async { Future<PlayHistory> getPosition(EpisodeBrief episode) async {
var dbHelper = DBHelper(); var dbHelper = DBHelper();
@ -401,7 +404,6 @@ class _MenuBarState extends State<MenuBar> {
if (result == 1 && mounted) if (result == 1 && mounted)
setState(() { setState(() {
_liked = false; _liked = false;
// _like = 0;
}); });
return result; return result;
} }
@ -526,12 +528,10 @@ class _MenuBarState extends State<MenuBar> {
}, },
), ),
DownloadButton(episode: widget.episodeItem), DownloadButton(episode: widget.episodeItem),
Selector<AudioPlayerNotifier, List<String>>( Selector<AudioPlayerNotifier, List<EpisodeBrief>>(
selector: (_, audio) => audio.queue.playlist selector: (_, audio) => audio.queue.playlist,
.map((e) => e.enclosureUrl)
.toList(),
builder: (_, data, __) { builder: (_, data, __) {
return data.contains(widget.episodeItem.enclosureUrl) return data.contains(widget.episodeItem)
? _buttonOnMenu( ? _buttonOnMenu(
Icon(Icons.playlist_add_check, Icon(Icons.playlist_add_check,
color: Theme.of(context).accentColor), () { color: Theme.of(context).accentColor), () {

View File

@ -2,7 +2,6 @@ import 'dart:ui';
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:flutter_downloader/flutter_downloader.dart';
import 'package:permission_handler/permission_handler.dart'; import 'package:permission_handler/permission_handler.dart';
@ -13,6 +12,7 @@ import '../state/download_state.dart';
import '../state/audiostate.dart'; import '../state/audiostate.dart';
import '../state/settingstate.dart'; import '../state/settingstate.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
import '../util/general_dialog.dart';
class DownloadButton extends StatefulWidget { class DownloadButton extends StatefulWidget {
final EpisodeBrief episode; final EpisodeBrief episode;
@ -107,54 +107,31 @@ class _DownloadButtonState extends State<DownloadButton> {
Future<bool> _useDataConfirem() async { Future<bool> _useDataConfirem() async {
bool ifUseData = false; bool ifUseData = false;
await showGeneralDialog( await generalDialog(
context: context, context,
barrierDismissible: true, title: Text('Cellular data warn'),
barrierLabel: MaterialLocalizations.of(context).modalBarrierDismissLabel, content: Text('Are you sure you want to use cellular data to download?'),
barrierColor: Colors.black54, actions: <Widget>[
transitionDuration: const Duration(milliseconds: 200), FlatButton(
pageBuilder: (BuildContext context, Animation animaiton, onPressed: () {
Animation secondaryAnimation) => Navigator.of(context).pop();
AnnotatedRegion<SystemUiOverlayStyle>( },
value: SystemUiOverlayStyle( child: Text(
statusBarIconBrightness: Brightness.light, 'CANCEL',
systemNavigationBarColor: style: TextStyle(color: Colors.grey[600]),
Theme.of(context).brightness == Brightness.light ),
? Color.fromRGBO(113, 113, 113, 1)
: Color.fromRGBO(15, 15, 15, 1),
), ),
child: AlertDialog( FlatButton(
elevation: 1, onPressed: () {
shape: RoundedRectangleBorder( ifUseData = true;
borderRadius: BorderRadius.all(Radius.circular(10.0))), Navigator.of(context).pop();
titlePadding: },
EdgeInsets.only(top: 20, left: 20, right: 100, bottom: 20), child: Text(
title: Text('Cellular data warn'), 'CONFIRM',
content: style: TextStyle(color: Colors.red),
Text('Are you sure you want to use cellular data to download?'), ),
actions: <Widget>[ )
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),
),
)
],
),
),
); );
return ifUseData; return ifUseData;
} }

View File

@ -8,9 +8,13 @@ import 'intl/messages_all.dart';
// Made by Localizely // Made by Localizely
// ************************************************************************** // **************************************************************************
// ignore_for_file: non_constant_identifier_names, lines_longer_than_80_chars
class S { class S {
S(); S();
static S current;
static const AppLocalizationDelegate delegate = static const AppLocalizationDelegate delegate =
AppLocalizationDelegate(); AppLocalizationDelegate();
@ -19,7 +23,9 @@ class S {
final localeName = Intl.canonicalizedLocale(name); final localeName = Intl.canonicalizedLocale(name);
return initializeMessages(localeName).then((_) { return initializeMessages(localeName).then((_) {
Intl.defaultLocale = localeName; Intl.defaultLocale = localeName;
return S(); S.current = S();
return S.current;
}); });
} }

View File

@ -6,7 +6,7 @@ import 'package:line_icons/line_icons.dart';
import '../util/context_extension.dart'; import '../util/context_extension.dart';
const String version = '0.3.3'; const String version = '0.3.4';
class AboutApp extends StatelessWidget { class AboutApp extends StatelessWidget {
_launchUrl(String url) async { _launchUrl(String url) async {

View File

@ -238,7 +238,7 @@ class _SearchListState extends State<SearchList> {
Future<List> _getList(String searchText, int nextOffset) async { Future<List> _getList(String searchText, int nextOffset) async {
String apiKey = environment['apiKey']; String apiKey = environment['apiKey'];
String url = "https://listen-api.listennotes.com/api/v2/search?q=" + String url = "https://listen-api.listennotes.com/api/v2/search?q=" +
searchText + Uri.encodeComponent(searchText) +
"&sort_by_date=0&type=podcast&offset=$nextOffset"; "&sort_by_date=0&type=podcast&offset=$nextOffset";
Response response = await Dio().get(url, Response response = await Dio().get(url,
options: Options(headers: { options: Options(headers: {

View File

@ -1050,168 +1050,183 @@ class _MyFavoriteState extends State<_MyFavorite>
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
super.build(context); super.build(context);
return FutureBuilder<List<EpisodeBrief>>( return Selector<AudioPlayerNotifier, bool>(
future: _getLikedRssItem(_top, _sortBy), selector: (_, audio) => audio.episodeState,
builder: (context, snapshot) { builder: (context, episodeState, child) {
if (snapshot.hasError) print(snapshot.error); return FutureBuilder<List<EpisodeBrief>>(
return (snapshot.hasData) future: _getLikedRssItem(_top, _sortBy),
? snapshot.data.length == 0 builder: (context, snapshot) {
? Padding( if (snapshot.hasError) print(snapshot.error);
padding: EdgeInsets.only(top: 150), return (snapshot.hasData)
child: Column( ? snapshot.data.length == 0
mainAxisAlignment: MainAxisAlignment.start, ? Padding(
children: [ padding: EdgeInsets.only(top: 150),
Icon(LineIcons.heartbeat_solid, child: Column(
size: 80, color: Colors.grey[500]), mainAxisAlignment: MainAxisAlignment.start,
Padding(padding: EdgeInsets.symmetric(vertical: 10)), children: [
Text( Icon(LineIcons.heartbeat_solid,
'No episode collected yet', size: 80, color: Colors.grey[500]),
style: TextStyle(color: Colors.grey[500]), Padding(
padding: EdgeInsets.symmetric(vertical: 10)),
Text(
'No episode collected yet',
style: TextStyle(color: Colors.grey[500]),
)
],
),
) )
], : NotificationListener<ScrollNotification>(
), onNotification: (ScrollNotification scrollInfo) {
) if (scrollInfo.metrics.pixels ==
: NotificationListener<ScrollNotification>( scrollInfo.metrics.maxScrollExtent &&
onNotification: (ScrollNotification scrollInfo) { snapshot.data.length == _top)
if (scrollInfo.metrics.pixels == _loadMoreEpisode();
scrollInfo.metrics.maxScrollExtent && return true;
snapshot.data.length == _top) _loadMoreEpisode(); },
return true; child: CustomScrollView(
}, key: PageStorageKey<String>('favorite'),
child: CustomScrollView( slivers: <Widget>[
key: PageStorageKey<String>('favorite'), SliverToBoxAdapter(
slivers: <Widget>[ child: Container(
SliverToBoxAdapter( height: 40,
child: Container( color: context.primaryColor,
height: 40, child: Row(
color: context.primaryColor, children: <Widget>[
child: Row( Material(
children: <Widget>[ color: Colors.transparent,
Material( child: PopupMenuButton<int>(
color: Colors.transparent, shape: RoundedRectangleBorder(
child: PopupMenuButton<int>( borderRadius: BorderRadius.all(
shape: RoundedRectangleBorder( Radius.circular(10))),
borderRadius: BorderRadius.all( elevation: 1,
Radius.circular(10))), tooltip: 'Sort By',
elevation: 1, child: Container(
tooltip: 'Sort By', height: 50,
child: Container(
height: 50,
padding: EdgeInsets.symmetric(
horizontal: 20),
child: Row(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
Text('Sory by'),
Padding(
padding: EdgeInsets.symmetric( padding: EdgeInsets.symmetric(
horizontal: 5), horizontal: 20),
child: Row(
mainAxisSize:
MainAxisSize.min,
children: <Widget>[
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( PopupMenuItem(
_sortBy == 0 value: 1,
? LineIcons child: Text('Like Date'),
.cloud_download_alt_solid
: LineIcons.heartbeat_solid,
size: 18,
) )
], ],
)), onSelected: (value) {
itemBuilder: (context) => [ if (value == 0)
PopupMenuItem( setState(() => _sortBy = 0);
value: 0, else if (value == 1)
child: Text('Update Date'), 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); EpisodeGrid(
else if (value == 1) episodes: snapshot.data,
setState(() => _sortBy = 1); layout: _layout,
}, initNum: 0,
), ),
), SliverList(
Spacer(), delegate: SliverChildBuilderDelegate(
Material( (BuildContext context, int index) {
color: Colors.transparent, return _loadMore
child: IconButton( ? Container(
padding: EdgeInsets.zero, height: 2,
onPressed: () { child: LinearProgressIndicator())
if (_layout == Layout.three) : Center();
setState(() { },
_layout = Layout.one; childCount: 1,
}); ),
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,
), ),
), )
], : Center();
), },
) );
: Center(); });
},
);
} }
@override @override

View File

@ -12,6 +12,7 @@ import 'package:feature_discovery/feature_discovery.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
import '../state/podcast_group.dart'; import '../state/podcast_group.dart';
import '../state/subscribe_podcast.dart'; import '../state/subscribe_podcast.dart';
import '../state/download_state.dart';
import '../type/podcastlocal.dart'; import '../type/podcastlocal.dart';
import '../state/audiostate.dart'; import '../state/audiostate.dart';
import '../util/custompaint.dart'; import '../util/custompaint.dart';
@ -19,6 +20,7 @@ import '../util/pageroute.dart';
import '../util/colorize.dart'; import '../util/colorize.dart';
import '../util/context_extension.dart'; import '../util/context_extension.dart';
import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import '../episodes/episodedetail.dart'; import '../episodes/episodedetail.dart';
import '../podcasts/podcastdetail.dart'; import '../podcasts/podcastdetail.dart';
import '../podcasts/podcastmanage.dart'; import '../podcasts/podcastmanage.dart';
@ -478,11 +480,61 @@ class ShowEpisode extends StatelessWidget {
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
} }
Future<int> _isListened(EpisodeBrief episode) async {
DBHelper dbHelper = DBHelper();
return await dbHelper.isListened(episode.enclosureUrl);
}
Future<bool> _isLiked(EpisodeBrief episode) async {
DBHelper dbHelper = DBHelper();
return await dbHelper.isLiked(episode.enclosureUrl);
}
Future<List<int>> _getEpisodeMenu() async {
KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey);
List<int> list = await popupMenuStorage.getMenu();
return list;
}
Future<bool> _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<int> _saveLiked(String url) async {
var dbHelper = DBHelper();
int result = await dbHelper.setLiked(url);
return result;
}
Future<int> _setUnliked(String url) async {
var dbHelper = DBHelper();
int result = await dbHelper.setUniked(url);
return result;
}
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, _showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context,
bool isPlaying, bool isInPlaylist) async { bool isPlaying, bool isInPlaylist) async {
var audio = Provider.of<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
double left = offset.dx; double left = offset.dx;
double top = offset.dy; double top = offset.dy;
bool isLiked, isDownload;
int isListened;
var downloader = Provider.of<DownloadState>(context, listen: false);
List<int> 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<int>( await showMenu<int>(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(Radius.circular(10))),
@ -506,39 +558,134 @@ class ShowEpisode extends StatelessWidget {
], ],
), ),
), ),
PopupMenuItem( menuList.contains(1)
value: 1, ? PopupMenuItem(
child: Row( value: 1,
children: <Widget>[ child: Row(
Icon( children: <Widget>[
LineIcons.clock_solid, Icon(
color: Colors.red, LineIcons.clock_solid,
), color: Colors.red,
Padding( ),
padding: EdgeInsets.symmetric(horizontal: 2), Padding(
), padding: EdgeInsets.symmetric(horizontal: 2),
!isInPlaylist ? Text('Later') : Text('Remove') ),
], !isInPlaylist ? Text('Later') : Text('Remove')
)), ],
))
: null,
menuList.contains(2)
? PopupMenuItem(
value: 2,
child: Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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, elevation: 5.0,
).then((value) { ).then((value) async {
if (value == 0) { switch (value) {
if (!isPlaying) audio.episodeLoad(episode); case 0:
} else if (value == 1) { if (!isPlaying) audio.episodeLoad(episode);
if (!isInPlaylist) { break;
audio.addToPlaylist(episode); case 1:
Fluttertoast.showToast( if (!isInPlaylist) {
msg: 'Added to playlist', audio.addToPlaylist(episode);
gravity: ToastGravity.BOTTOM, Fluttertoast.showToast(
); msg: 'Added to playlist',
} else { gravity: ToastGravity.BOTTOM,
audio.delFromPlaylist(episode); );
Fluttertoast.showToast( } else {
msg: 'Removed from playlist', audio.delFromPlaylist(episode);
gravity: ToastGravity.BOTTOM, 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;
} }
}); });
} }

View File

@ -22,6 +22,8 @@ const String podcastLayoutKey = 'podcastLayoutKey';
const String recentLayoutKey = 'recentLayoutKey'; const String recentLayoutKey = 'recentLayoutKey';
const String favLayoutKey = 'favLayoutKey'; const String favLayoutKey = 'favLayoutKey';
const String downloadLayoutKey = 'downloadLayoutKey'; const String downloadLayoutKey = 'downloadLayoutKey';
const String autoDownloadNetworkKey = 'autoDownloadNetwork';
const String episodePopupMenuKey = 'episodePopupMenuKey';
class KeyValueStorage { class KeyValueStorage {
final String key; final String key;
@ -88,4 +90,18 @@ class KeyValueStorage {
} }
return prefs.getString(key); return prefs.getString(key);
} }
saveMenu(List<int> list) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setStringList(key, list.map((e) => e.toString()).toList());
}
Future<List<int>> getMenu() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
if (prefs.getStringList(key) == null) {
await prefs.setStringList(key, ['0', '1', '12', '13', '14']);
}
List<String> list = prefs.getStringList(key);
return list.map((e) => int.parse(e)).toList();
}
} }

View File

@ -382,7 +382,6 @@ class DBHelper {
String month = mmDd.stringMatch(pubDate); String month = mmDd.stringMatch(pubDate);
date = DateFormat('yyyy-MM-dd HH:mm', 'en_US') date = DateFormat('yyyy-MM-dd HH:mm', 'en_US')
.parse(month + ' ' + time); .parse(month + ' ' + time);
print(month);
print(date.toString()); print(date.toString());
} else { } else {
date = DateTime.now(); date = DateTime.now();
@ -832,36 +831,36 @@ class DBHelper {
return episodes; return episodes;
} }
Future<List<EpisodeBrief>> gettNewRssItem(String id) async { //Future<List<EpisodeBrief>> getNewRssItem(String id) async {
var dbClient = await database; // var dbClient = await database;
List<EpisodeBrief> episodes = []; // List<EpisodeBrief> episodes = [];
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.title as feed_title, E.duration, E.explicit, E.liked, // 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 // 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 // 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 """, // WHERE is_new = 1 AND downloaded != 'ND' AND P.id = ?ORDER BY E.milliseconds DESC """,
[id], // [id],
); // );
for (int x = 0; x < list.length; x++) { // for (int x = 0; x < list.length; x++) {
episodes.add(EpisodeBrief( // episodes.add(EpisodeBrief(
list[x]['title'], // list[x]['title'],
list[x]['enclosure_url'], // list[x]['enclosure_url'],
list[x]['enclosure_length'], // list[x]['enclosure_length'],
list[x]['milliseconds'], // list[x]['milliseconds'],
list[x]['feed_title'], // list[x]['feed_title'],
list[x]['primaryColor'], // list[x]['primaryColor'],
list[x]['liked'], // list[x]['liked'],
list[x]['downloaded'], // list[x]['downloaded'],
list[x]['duration'], // list[x]['duration'],
list[x]['explicit'], // list[x]['explicit'],
list[x]['imagePath'], // list[x]['imagePath'],
list[x]['media_id'], // list[x]['media_id'],
list[x]['is_new'], // list[x]['is_new'],
list[x]['skip_seconds'])); // list[x]['skip_seconds']));
} // }
return episodes; // return episodes;
} //}
Future<int> removeAllNewMark() async { Future<int> removeAllNewMark() async {
var dbClient = await database; var dbClient = await database;
@ -994,6 +993,13 @@ class DBHelper {
return list.first['liked'] == 0 ? false : true; return list.first['liked'] == 0 ? false : true;
} }
Future<bool> isDownloaded(String url) async {
var dbClient = await database;
List<Map> list = await dbClient
.rawQuery("SELECT downloaded FROM Episodes WHERE enclosure_url = ?", [url]);
return list.first['downloaded'] == 'ND' ? false: true;
}
Future<int> saveDownloaded(String url, String id) async { Future<int> saveDownloaded(String url, String id) async {
var dbClient = await database; var dbClient = await database;
int milliseconds = DateTime.now().millisecondsSinceEpoch; int milliseconds = DateTime.now().millisecondsSinceEpoch;

View File

@ -1,10 +1,12 @@
import 'dart:io'; import 'dart:io';
import 'dart:async'; import 'dart:async';
import 'package:connectivity/connectivity.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:html/parser.dart'; import 'package:html/parser.dart';
import 'package:tsacdop/state/download_state.dart';
import 'package:url_launcher/url_launcher.dart'; import 'package:url_launcher/url_launcher.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:flutter_linkify/flutter_linkify.dart';
@ -56,6 +58,26 @@ class _PodcastDetailState extends State<PodcastDetail> {
msg: 'Updated $result Episodes', msg: 'Updated $result Episodes',
gravity: ToastGravity.TOP, 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<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
episodes.forEach((episode) {
DownloadState().startTask(episode, showNotification: false);
});
} else if (result == ConnectivityResult.wifi) {
List<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
episodes.forEach((episode) {
DownloadState().startTask(episode, showNotification: false);
});
}
}
// Provider.of<GroupList>(context, listen: false) // Provider.of<GroupList>(context, listen: false)
// .updatePodcast(podcastLocal.id); // .updatePodcast(podcastLocal.id);
} else { } else {

View File

@ -5,14 +5,12 @@ import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:fluttertoast/fluttertoast.dart'; import 'package:fluttertoast/fluttertoast.dart';
import 'package:permission_handler/permission_handler.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:line_icons/line_icons.dart';
import '../state/podcast_group.dart'; import '../state/podcast_group.dart';
import '../type/podcastlocal.dart'; import '../type/podcastlocal.dart';
import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/sqflite_localpodcast.dart';
import '../podcasts/podcastdetail.dart';
import '../util/pageroute.dart';
import '../util/colorize.dart'; import '../util/colorize.dart';
import '../util/duraiton_picker.dart'; import '../util/duraiton_picker.dart';
import '../util/context_extension.dart'; import '../util/context_extension.dart';
@ -98,8 +96,11 @@ class _PodcastCardState extends State<PodcastCard>
} }
_setAutoDownload(String id, bool boo) async { _setAutoDownload(String id, bool boo) async {
DBHelper dbHelper = DBHelper(); bool permission = await _checkPermmison();
await dbHelper.saveAutoDownload(id, boo); if (permission) {
DBHelper dbHelper = DBHelper();
await dbHelper.saveAutoDownload(id, boo);
}
} }
Future<bool> _getAutoDownload(String id) async { Future<bool> _getAutoDownload(String id) async {
@ -107,6 +108,21 @@ class _PodcastCardState extends State<PodcastCard>
return await dbHelper.getAutoDownload(id); return await dbHelper.getAutoDownload(id);
} }
Future<bool> _checkPermmison() async {
PermissionStatus permission = await Permission.storage.status;
if (permission != PermissionStatus.granted) {
Map<Permission, PermissionStatus> permissions =
await [Permission.storage].request();
if (permissions[Permission.storage] == PermissionStatus.granted) {
return true;
} else {
return false;
}
} else {
return true;
}
}
String _stringForSeconds(double seconds) { String _stringForSeconds(double seconds) {
if (seconds == null) return null; if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
@ -344,13 +360,27 @@ class _PodcastCardState extends State<PodcastCard>
initialData: false, initialData: false,
builder: (context, snapshot) { builder: (context, snapshot) {
return _buttonOnMenu( return _buttonOnMenu(
icon: Icon( icon: Container(
LineIcons.cloud_download_alt_solid, child: Icon(Icons.done_all,
size: _value == 0 ? 1 : 20 * _value, size: _value * 15,
color: snapshot.data color: snapshot.data
? context.accentColor ? Colors.white
: null), : null),
tooltip: 'AutoDownload', 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: () { onTap: () {
_setAutoDownload(widget.podcastLocal.id, _setAutoDownload(widget.podcastLocal.id,
!snapshot.data); !snapshot.data);

View File

@ -58,7 +58,6 @@ 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 {
print(episode.downloaded);
await FlutterDownloader.remove( await FlutterDownloader.remove(
taskId: episode.downloaded, shouldDeleteContent: true); taskId: episode.downloaded, shouldDeleteContent: true);
var dbHelper = DBHelper(); var dbHelper = DBHelper();
@ -212,7 +211,6 @@ class _DownloadsManageState extends State<DownloadsManage> {
value: _selectedList value: _selectedList
.contains(_episodes[index]), .contains(_episodes[index]),
onChanged: (bool boo) { onChanged: (bool boo) {
print(boo);
if (boo) { if (boo) {
setState(() => _selectedList setState(() => _selectedList
.add(_episodes[index])); .add(_episodes[index]));

View File

@ -117,7 +117,6 @@ class _PlayedHistoryState extends State<PlayedHistory>
systemNavigationBarColor: Theme.of(context).primaryColor, systemNavigationBarColor: Theme.of(context).primaryColor,
systemNavigationBarIconBrightness: systemNavigationBarIconBrightness:
Theme.of(context).accentColorBrightness, Theme.of(context).accentColorBrightness,
//statusBarColor: Theme.of(context).primaryColor,
), ),
child: Scaffold( child: Scaffold(
backgroundColor: Theme.of(context).primaryColor, backgroundColor: Theme.of(context).primaryColor,

View File

@ -5,6 +5,7 @@ import '../util/context_extension.dart';
import '../util/episodegrid.dart'; import '../util/episodegrid.dart';
import '../util/custompaint.dart'; import '../util/custompaint.dart';
import '../local_storage/key_value_storage.dart'; import '../local_storage/key_value_storage.dart';
import 'popup_menu.dart';
class LayoutSetting extends StatefulWidget { class LayoutSetting extends StatefulWidget {
const LayoutSetting({Key key}) : super(key: key); const LayoutSetting({Key key}) : super(key: key);
@ -23,7 +24,7 @@ class _LayoutSettingState extends State<LayoutSetting> {
Widget _gridOptions(BuildContext context, Widget _gridOptions(BuildContext context,
{String key, Layout layout, Layout option, double scale}) => {String key, Layout layout, Layout option, double scale}) =>
Padding( 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( child: InkWell(
onTap: () async { onTap: () async {
KeyValueStorage storage = KeyValueStorage(key); KeyValueStorage storage = KeyValueStorage(key);
@ -34,7 +35,9 @@ class _LayoutSettingState extends State<LayoutSetting> {
child: Container( child: Container(
height: 30, height: 30,
width: 50, width: 50,
color: layout == option ? context.accentColor : Colors.transparent, color: layout == option
? context.accentColor
: context.primaryColorDark,
alignment: Alignment.center, alignment: Alignment.center,
child: SizedBox( child: SizedBox(
height: 10, height: 10,
@ -80,6 +83,32 @@ class _LayoutSettingState extends State<LayoutSetting> {
}); });
} }
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 @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
return AnnotatedRegion<SystemUiOverlayStyle>( return AnnotatedRegion<SystemUiOverlayStyle>(
@ -106,7 +135,7 @@ class _LayoutSettingState extends State<LayoutSetting> {
height: 30.0, height: 30.0,
padding: EdgeInsets.symmetric(horizontal: 80), padding: EdgeInsets.symmetric(horizontal: 80),
alignment: Alignment.centerLeft, alignment: Alignment.centerLeft,
child: Text('Default grid view', child: Text('Episode popup menu',
style: Theme.of(context) style: Theme.of(context)
.textTheme .textTheme
.bodyText1 .bodyText1
@ -118,44 +147,42 @@ class _LayoutSettingState extends State<LayoutSetting> {
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
children: <Widget>[ children: <Widget>[
ListTile( ListTile(
contentPadding: onTap: () => Navigator.push(
EdgeInsets.only(left: 80.0, right: 20, bottom: 10), context,
// leading: Icon(Icons.colorize), MaterialPageRoute(
title: Text( builder: (context) => PopupMenuSetting())),
'Podcast page', contentPadding: EdgeInsets.only(left: 80.0, right: 20),
), title: Text('Episode popup menu'),
subtitle: subtitle: Text('Change the menu when long tap episode'),
_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),
), ),
Divider(height: 2), 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: <Widget>[
_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)
]), ]),
], ],
), ),

View File

@ -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<PopupMenuSetting> {
Future<List<int>> _getEpisodeMenu() async {
KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey);
List<int> list = await popupMenuStorage.getMenu();
return list;
}
_saveEpisodeMene(List<int> list) async {
KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey);
await popupMenuStorage.saveMenu(list);
setState(() {});
}
Widget _popupMenuItem(List<int> 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<SystemUiOverlayStyle>(
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<List<int>>(
future: _getEpisodeMenu(),
initialData: [0, 1, 12, 13, 14],
builder: (context, snapshot) {
List<int> menu = snapshot.data;
return ListView(
shrinkWrap: true,
children: menu.map<Widget>((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(),
);
}),
],
),
)),
);
}
}

View File

@ -20,7 +20,7 @@ class _StorageSettingState extends State<StorageSetting>
Animation<double> _animation; Animation<double> _animation;
_getCacheMax() async { _getCacheMax() async {
int cache = await cacheStorage.getInt(); int cache = await cacheStorage.getInt();
int value = cache == 0 ? 500 : cache ~/ (1024 * 1024); int value = cache == 0 ? 200 : cache ~/ (1024 * 1024);
if (value > 100) { if (value > 100) {
_controller = AnimationController( _controller = AnimationController(
vsync: this, duration: Duration(milliseconds: value * 2)); vsync: this, duration: Duration(milliseconds: value * 2));
@ -38,6 +38,17 @@ class _StorageSettingState extends State<StorageSetting>
} }
} }
Future<bool> _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; double _value;
@override @override
@ -96,29 +107,50 @@ class _StorageSettingState extends State<StorageSetting>
shrinkWrap: true, shrinkWrap: true,
scrollDirection: Axis.vertical, scrollDirection: Axis.vertical,
children: <Widget>[ children: <Widget>[
ListTile( Selector<SettingState, bool>(
onTap: () => Navigator.push( selector: (_, settings) => settings.downloadUsingData,
context, builder: (_, data, __) {
MaterialPageRoute( return ListTile(
builder: (context) => DownloadsManage())), onTap: () => settings.downloadUsingData = !data,
contentPadding: EdgeInsets.only( contentPadding: EdgeInsets.only(
left: 80.0, right: 25, bottom: 10, top: 10), left: 80.0, right: 25, bottom: 10, top: 10),
title: Text('Ask before using cellular data'), title: Text('Ask before using cellular data'),
subtitle: Text( subtitle: Text(
'Ask to confirm when using cellular data to download episodes.'), 'Ask to confirm when using cellular data to download episodes.'),
trailing: Selector<SettingState, bool>( trailing: Switch(
selector: (_, settings) =>
settings.downloadUsingData,
builder: (_, data, __) {
return Switch(
value: data, value: data,
onChanged: (value) => onChanged: (value) =>
settings.downloadUsingData = value, settings.downloadUsingData = value,
); ),
}, );
), },
), ),
Divider(height: 2), Divider(height: 2),
FutureBuilder<bool>(
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<StorageSetting>
builder: (context) => DownloadsManage())), builder: (context) => DownloadsManage())),
contentPadding: EdgeInsets.symmetric(horizontal: 80.0), contentPadding: EdgeInsets.symmetric(horizontal: 80.0),
title: Text('Downloads'), title: Text('Downloads'),
subtitle: Text('Manage doanloaded audio files'), subtitle: Text('Manage downloaded audio files'),
), ),
Divider(height: 2), Divider(height: 2),
ListTile( ListTile(
@ -175,8 +207,10 @@ class _StorageSettingState extends State<StorageSetting>
left: 60.0, right: 20.0, bottom: 10.0), left: 60.0, right: 20.0, bottom: 10.0),
child: SliderTheme( child: SliderTheme(
data: Theme.of(context).sliderTheme.copyWith( data: Theme.of(context).sliderTheme.copyWith(
showValueIndicator: ShowValueIndicator.always, showValueIndicator: ShowValueIndicator.always,
), trackHeight: 2,
thumbShape:
RoundSliderThumbShape(enabledThumbRadius: 6)),
child: Slider( child: Slider(
label: '${_value ~/ 100 * 100} Mb', label: '${_value ~/ 100 * 100} Mb',
activeColor: context.accentColor, activeColor: context.accentColor,

View File

@ -155,6 +155,8 @@ class AudioPlayerNotifier extends ChangeNotifier {
int _currentPosition; int _currentPosition;
double _currentSpeed = 1; double _currentSpeed = 1;
BehaviorSubject<List<MediaItem>> queueSubject; BehaviorSubject<List<MediaItem>> queueSubject;
//Update episode card when setting changed
bool _episodeState = false;
BasicPlaybackState get audioState => _audioState; BasicPlaybackState get audioState => _audioState;
@ -176,6 +178,7 @@ class AudioPlayerNotifier extends ChangeNotifier {
int get timeLeft => _timeLeft; int get timeLeft => _timeLeft;
double get switchValue => _switchValue; double get switchValue => _switchValue;
double get currentSpeed => _currentSpeed; double get currentSpeed => _currentSpeed;
bool get episodeState => _episodeState;
set setSwitchValue(double value) { set setSwitchValue(double value) {
_switchValue = value; _switchValue = value;
@ -193,6 +196,11 @@ class AudioPlayerNotifier extends ChangeNotifier {
notifyListeners(); notifyListeners();
} }
set setEpisodeState(bool boo) {
_episodeState = !_episodeState;
notifyListeners();
}
Future _getAutoPlay() async { Future _getAutoPlay() async {
int i = await autoPlayStorage.getInt(); int i = await autoPlayStorage.getInt();
_autoPlay = i == 0 ? true : false; _autoPlay = i == 0 ? true : false;
@ -315,7 +323,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
queueSubject.addStream( queueSubject.addStream(
AudioService.queueStream.distinct().where((event) => event != null)); AudioService.queueStream.distinct().where((event) => event != null));
queueSubject.stream.listen((event) { queueSubject.stream.listen((event) {
print(event.length);
if (event.length == _queue.playlist.length - 1 && if (event.length == _queue.playlist.length - 1 &&
_audioState == BasicPlaybackState.skippingToNext) { _audioState == BasicPlaybackState.skippingToNext) {
if (event.length == 0 || _stopOnComplete) { if (event.length == 0 || _stopOnComplete) {
@ -506,7 +513,6 @@ class AudioPlayerNotifier extends ChangeNotifier {
} }
sliderSeek(double val) async { sliderSeek(double val) async {
print(val.toString());
if (_audioState != BasicPlaybackState.connecting && if (_audioState != BasicPlaybackState.connecting &&
_audioState != BasicPlaybackState.none) { _audioState != BasicPlaybackState.none) {
_noSlide = false; _noSlide = false;
@ -633,6 +639,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
AudioPlayer _audioPlayer = AudioPlayer(); AudioPlayer _audioPlayer = AudioPlayer();
Completer _completer = Completer(); Completer _completer = Completer();
BasicPlaybackState _skipState; BasicPlaybackState _skipState;
bool _lostFocus;
bool _playing; bool _playing;
bool _stopAtEnd; bool _stopAtEnd;
int cacheMax; int cacheMax;
@ -662,7 +669,7 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
Future<void> onStart() async { Future<void> onStart() async {
_stopAtEnd = false; _stopAtEnd = false;
_lostFocus = false;
var playerStateSubscription = _audioPlayer.playbackStateStream var playerStateSubscription = _audioPlayer.playbackStateStream
.where((state) => state == AudioPlaybackState.completed) .where((state) => state == AudioPlaybackState.completed)
.listen((state) { .listen((state) {
@ -792,9 +799,12 @@ class AudioPlayerTask extends BackgroundAudioTask {
@override @override
void onPause() { void onPause() {
if (_skipState == null) { if (_skipState == null) {
if (_playing == null) {} if (_playing == null) {
_playing = false; } else if (_audioPlayer.playbackEvent.state ==
_audioPlayer.pause(); AudioPlaybackState.playing) {
_playing = false;
_audioPlayer.pause();
}
} }
} }
@ -857,32 +867,45 @@ class AudioPlayerTask extends BackgroundAudioTask {
milliseconds: AudioServiceBackground.state.position + 30 * 1000)); milliseconds: AudioServiceBackground.state.position + 30 * 1000));
} }
@override
void onRewind() {
_audioPlayer.seek(Duration(
milliseconds: AudioServiceBackground.state.position - 10 * 1000));
}
@override @override
void onAudioFocusLost() { void onAudioFocusLost() {
if (_skipState == null) { if (_skipState == null) {
if (_playing == null || if (_playing == null) {
_audioPlayer.playbackState == AudioPlaybackState.none || } else if (_audioPlayer.playbackEvent.state ==
_audioPlayer.playbackState == AudioPlaybackState.connecting) {} AudioPlaybackState.playing) {
_playing = false; _playing = false;
_audioPlayer.pause(); _lostFocus = true;
_audioPlayer.pause();
}
} }
} }
@override @override
void onAudioBecomingNoisy() { void onAudioBecomingNoisy() {
if (_skipState == null) { if (_skipState == null) {
if (_playing == null) {} if (_playing == null) {
_playing = false; } else if (_audioPlayer.playbackEvent.state ==
_audioPlayer.pause(); AudioPlaybackState.playing) {
_playing = false;
_audioPlayer.pause();
}
} }
} }
@override @override
void onAudioFocusGained() { void onAudioFocusGained() {
if (_skipState == null) { if (_skipState == null) {
if (_playing == null) {} if (_lostFocus) {
_playing = true; _lostFocus = false;
_audioPlayer.play(); _playing = true;
_audioPlayer.play();
}
} }
} }

View File

@ -93,7 +93,8 @@ class DownloadState extends ChangeNotifier {
final completeTask = await FlutterDownloader.loadTasksWithRawQuery( final completeTask = await FlutterDownloader.loadTasksWithRawQuery(
query: "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'"); query: "SELECT * FROM task WHERE task_id = '${episodeTask.taskId}'");
String filePath = 'file://' + String filePath = 'file://' +
path.join(completeTask.first.savedDir, completeTask.first.filename); path.join(completeTask.first.savedDir,
Uri.encodeComponent(completeTask.first.filename));
dbHelper.saveMediaId( dbHelper.saveMediaId(
episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId); episodeTask.episode.enclosureUrl, filePath, episodeTask.taskId);
EpisodeBrief episode = EpisodeBrief episode =
@ -124,7 +125,7 @@ class DownloadState extends ChangeNotifier {
super.dispose(); super.dispose();
} }
Future startTask(EpisodeBrief episode) async { Future startTask(EpisodeBrief episode, {bool showNotification = true}) async {
final dir = await getExternalStorageDirectory(); final dir = await getExternalStorageDirectory();
String localPath = path.join(dir.path, episode.feedTitle); String localPath = path.join(dir.path, episode.feedTitle);
final saveDir = Directory(localPath); final saveDir = Directory(localPath);
@ -137,10 +138,7 @@ class DownloadState extends ChangeNotifier {
now.month.toString() + now.month.toString() +
now.day.toString() + now.day.toString() +
now.second.toString(); now.second.toString();
String title = episode.title.trim().substring(0, 1) == '#' String fileName = episode.title +
? episode.title.trim().substring(1)
: episode.title.trim();
String fileName = title +
datePlus + datePlus +
'.' + '.' +
episode.enclosureUrl.split('/').last.split('.').last; episode.enclosureUrl.split('/').last.split('.').last;
@ -148,12 +146,12 @@ class DownloadState extends ChangeNotifier {
fileName: fileName, fileName: fileName,
url: episode.enclosureUrl, url: episode.enclosureUrl,
savedDir: localPath, savedDir: localPath,
showNotification: true, showNotification: showNotification,
openFileFromNotification: false, openFileFromNotification: false,
); );
_episodeTasks.add(EpisodeTask(episode, taskId)); _episodeTasks.add(EpisodeTask(episode, taskId));
var dbHelper = DBHelper(); var dbHelper = DBHelper();
await dbHelper.saveDownloaded(taskId, episode.enclosureUrl); await dbHelper.saveDownloaded(episode.enclosureUrl, taskId);
notifyListeners(); notifyListeners();
} }

View File

@ -2,10 +2,13 @@ import 'dart:isolate';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_isolate/flutter_isolate.dart'; import 'package:flutter_isolate/flutter_isolate.dart';
import 'package:tsacdop/local_storage/key_value_storage.dart'; import 'package:connectivity/connectivity.dart';
import 'package:tsacdop/local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import '../local_storage/sqflite_localpodcast.dart';
import '../type/podcastlocal.dart'; import '../type/podcastlocal.dart';
import '../type/episodebrief.dart';
import 'download_state.dart';
enum RefreshState { none, fetch, error } enum RefreshState { none, fetch, error }
@ -68,13 +71,32 @@ class RefreshWorker extends ChangeNotifier {
Future<void> refreshIsolateEntryPoint(SendPort sendPort) async { Future<void> refreshIsolateEntryPoint(SendPort sendPort) async {
KeyValueStorage refreshstorage = KeyValueStorage(refreshdateKey); KeyValueStorage refreshstorage = KeyValueStorage(refreshdateKey);
KeyValueStorage autoDownloadStorage = KeyValueStorage(autoDownloadNetworkKey);
await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch); await refreshstorage.saveInt(DateTime.now().millisecondsSinceEpoch);
var dbHelper = DBHelper(); var dbHelper = DBHelper();
List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll(); List<PodcastLocal> podcastList = await dbHelper.getPodcastLocalAll();
//int i = 0; //int i = 0;
await Future.forEach<PodcastLocal>(podcastList, (podcastLocal) async { await Future.forEach<PodcastLocal>(podcastList, (podcastLocal) async {
sendPort.send([podcastLocal.title, 1]); 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<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
episodes.forEach((episode) {
DownloadState().startTask(episode, showNotification: false);
});
} else if (result == ConnectivityResult.wifi) {
List<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
episodes.forEach((episode) {
DownloadState().startTask(episode, showNotification: false);
});
}
}
print('Refresh ' + podcastLocal.title); print('Refresh ' + podcastLocal.title);
}); });
// KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount'); // KeyValueStorage refreshcountstorage = KeyValueStorage('refreshcount');

View File

@ -1,8 +1,8 @@
import 'dart:io'; import 'dart:io';
import 'package:connectivity/connectivity.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:workmanager/workmanager.dart'; import 'package:workmanager/workmanager.dart';
import 'package:flutter_downloader/flutter_downloader.dart';
import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart'; import '../local_storage/key_value_storage.dart';
@ -24,11 +24,23 @@ void callbackDispatcher() {
await dbHelper.updatePodcastRss(podcastLocal, removeMark: lastWork); await dbHelper.updatePodcastRss(podcastLocal, removeMark: lastWork);
bool autoDownload = await dbHelper.getAutoDownload(podcastLocal.id); bool autoDownload = await dbHelper.getAutoDownload(podcastLocal.id);
if (autoDownload && updateCount > 0) { if (autoDownload && updateCount > 0) {
List<EpisodeBrief> episodes = var result = await Connectivity().checkConnectivity();
await dbHelper.getNewEpisodes(podcastLocal.id); KeyValueStorage autoDownloadStorage =
episodes.forEach((episode) { KeyValueStorage(autoDownloadNetworkKey);
DownloadState().startTask(episode); int autoDownloadNetwork = await autoDownloadStorage.getInt();
}); if (autoDownloadNetwork == 1) {
List<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
episodes.forEach((episode) {
DownloadState().startTask(episode, showNotification: false);
});
} else if (result == ConnectivityResult.wifi) {
List<EpisodeBrief> episodes =
await dbHelper.getNewEpisodes(podcastLocal.id);
episodes.forEach((episode) {
DownloadState().startTask(episode, showNotification: false);
});
}
} }
print('Refresh ' + podcastLocal.title); print('Refresh ' + podcastLocal.title);
}); });

View File

@ -5,6 +5,7 @@ extension ContextExtension on BuildContext{
Color get accentColor => Theme.of(this).accentColor; Color get accentColor => Theme.of(this).accentColor;
Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor; Color get scaffoldBackgroundColor => Theme.of(this).scaffoldBackgroundColor;
Color get primaryColorDark => Theme.of(this).primaryColorDark; Color get primaryColorDark => Theme.of(this).primaryColorDark;
Color get textColor => Theme.of(this).textTheme.bodyText1.color;
Brightness get brightness => Theme.of(this).brightness; Brightness get brightness => Theme.of(this).brightness;
double get width => MediaQuery.of(this).size.width; double get width => MediaQuery.of(this).size.width;
double get height => MediaQuery.of(this).size.width; double get height => MediaQuery.of(this).size.width;

View File

@ -11,9 +11,11 @@ import 'package:auto_animated/auto_animated.dart';
import 'open_container.dart'; import 'open_container.dart';
import '../state/audiostate.dart'; import '../state/audiostate.dart';
import '../state/download_state.dart';
import '../type/episodebrief.dart'; import '../type/episodebrief.dart';
import '../episodes/episodedetail.dart'; import '../episodes/episodedetail.dart';
import '../local_storage/sqflite_localpodcast.dart'; import '../local_storage/sqflite_localpodcast.dart';
import '../local_storage/key_value_storage.dart';
import 'colorize.dart'; import 'colorize.dart';
import 'context_extension.dart'; import 'context_extension.dart';
import 'custompaint.dart'; import 'custompaint.dart';
@ -51,6 +53,39 @@ class EpisodeGrid extends StatelessWidget {
return await dbHelper.isLiked(episode.enclosureUrl); return await dbHelper.isLiked(episode.enclosureUrl);
} }
Future<List<int>> _getEpisodeMenu() async {
KeyValueStorage popupMenuStorage = KeyValueStorage(episodePopupMenuKey);
List<int> list = await popupMenuStorage.getMenu();
return list;
}
Future<bool> _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<int> _saveLiked(String url) async {
var dbHelper = DBHelper();
int result = await dbHelper.setLiked(url);
return result;
}
Future<int> _setUnliked(String url) async {
var dbHelper = DBHelper();
int result = await dbHelper.setUniked(url);
return result;
}
String _stringForSeconds(double seconds) { String _stringForSeconds(double seconds) {
if (seconds == null) return null; if (seconds == null) return null;
return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}'; return '${(seconds ~/ 60)}:${(seconds.truncate() % 60).toString().padLeft(2, '0')}';
@ -171,15 +206,23 @@ class EpisodeGrid extends StatelessWidget {
color: color, color: color,
fontStyle: FontStyle.italic), fontStyle: FontStyle.italic),
); );
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
double _width = MediaQuery.of(context).size.width; double _width = context.width;
Offset _offset; Offset _offset;
_showPopupMenu(Offset offset, EpisodeBrief episode, BuildContext context, _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<AudioPlayerNotifier>(context, listen: false); var audio = Provider.of<AudioPlayerNotifier>(context, listen: false);
var downloader = Provider.of<DownloadState>(context, listen: false);
double left = offset.dx; double left = offset.dx;
double top = offset.dy; double top = offset.dy;
List<int> 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<int>( await showMenu<int>(
shape: RoundedRectangleBorder( shape: RoundedRectangleBorder(
borderRadius: BorderRadius.all(Radius.circular(10))), borderRadius: BorderRadius.all(Radius.circular(10))),
@ -203,39 +246,134 @@ class EpisodeGrid extends StatelessWidget {
], ],
), ),
), ),
PopupMenuItem( menuList.contains(1)
value: 1, ? PopupMenuItem(
child: Row( value: 1,
children: <Widget>[ child: Row(
Icon( children: <Widget>[
LineIcons.clock_solid, Icon(
color: Colors.red, LineIcons.clock_solid,
), color: Colors.cyan,
Padding( ),
padding: EdgeInsets.symmetric(horizontal: 2), Padding(
), padding: EdgeInsets.symmetric(horizontal: 2),
!isInPlaylist ? Text('Later') : Text('Remove') ),
], !isInPlaylist ? Text('Later') : Text('Remove')
)), ],
))
: null,
menuList.contains(2)
? PopupMenuItem(
value: 2,
child: Row(
children: <Widget>[
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: <Widget>[
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: <Widget>[
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, elevation: 5.0,
).then((value) { ).then((value) async {
if (value == 0) { switch (value) {
if (!isPlaying) audio.episodeLoad(episode); case 0:
} else if (value == 1) { if (!isPlaying) audio.episodeLoad(episode);
if (!isInPlaylist) { break;
audio.addToPlaylist(episode); case 1:
Fluttertoast.showToast( if (!isInPlaylist) {
msg: 'Added to playlist', audio.addToPlaylist(episode);
gravity: ToastGravity.BOTTOM, Fluttertoast.showToast(
); msg: 'Added to playlist',
} else { gravity: ToastGravity.BOTTOM,
audio.delFromPlaylist(episode); );
Fluttertoast.showToast( } else {
msg: 'Removed from playlist', audio.delFromPlaylist(episode);
gravity: ToastGravity.BOTTOM, 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<double>(begin: index < initNum ? 0 : 1, end: 1) opacity: Tween<double>(begin: index < initNum ? 0 : 1, end: 1)
.animate(animation), .animate(animation),
child: Selector<AudioPlayerNotifier, child: Selector<AudioPlayerNotifier,
Tuple2<EpisodeBrief, List<String>>>( Tuple3<EpisodeBrief, List<String>, bool>>(
selector: (_, audio) => Tuple2(audio?.episode, selector: (_, audio) => Tuple3(
audio.queue.playlist.map((e) => e.enclosureUrl).toList()), audio?.episode,
audio.queue.playlist.map((e) => e.enclosureUrl).toList(),
audio.episodeState),
builder: (_, data, __) => OpenContainerWrapper( builder: (_, data, __) => OpenContainerWrapper(
episode: episodes[index], episode: episodes[index],
closedBuilder: (context, action, boo) => FutureBuilder<int>( closedBuilder: (context, action, boo) => FutureBuilder<int>(
@ -310,12 +450,13 @@ class EpisodeGrid extends StatelessWidget {
details.globalPosition.dx, details.globalPosition.dx,
details.globalPosition.dy), details.globalPosition.dy),
onLongPress: () => _showPopupMenu( onLongPress: () => _showPopupMenu(
_offset, _offset,
episodes[index], episodes[index],
context, context,
data.item1 == episodes[index], isPlaying: data.item1 == episodes[index],
data.item2 isInPlaylist: data.item2
.contains(episodes[index].enclosureUrl)), .contains(episodes[index].enclosureUrl),
),
onTap: action, onTap: action,
child: Container( child: Container(
padding: const EdgeInsets.all(8.0), padding: const EdgeInsets.all(8.0),

View File

@ -1,7 +1,7 @@
name: tsacdop name: tsacdop
description: An easy-use podacasts player. description: An easy-use podacasts player.
version: 0.3.3 version: 0.3.4
environment: environment:
sdk: ">=2.6.0 <3.0.0" sdk: ">=2.6.0 <3.0.0"