From 9fdd549d5c6f829cfb1bcd25590405784e0ef50d Mon Sep 17 00:00:00 2001 From: stonegate Date: Tue, 15 Sep 2020 19:48:22 +0800 Subject: [PATCH] Improve multi select feature. --- lib/episodes/episode_download.dart | 75 +++----- lib/home/home_groups.dart | 93 ++++++++-- lib/podcasts/podcast_detail.dart | 288 +++++++++++++++++++++++------ lib/state/download_state.dart | 2 +- lib/util/custom_widget.dart | 3 +- lib/util/episodegrid.dart | 75 +++++++- 6 files changed, 421 insertions(+), 115 deletions(-) diff --git a/lib/episodes/episode_download.dart b/lib/episodes/episode_download.dart index c67e356..d043677 100644 --- a/lib/episodes/episode_download.dart +++ b/lib/episodes/episode_download.dart @@ -8,6 +8,7 @@ import 'package:flutter_downloader/flutter_downloader.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'package:tsacdop/local_storage/key_value_storage.dart'; import '../state/audio_state.dart'; import '../state/download_state.dart'; @@ -26,33 +27,18 @@ class DownloadButton extends StatefulWidget { } class _DownloadButtonState extends State { - bool _permissionReady; - bool _usingData; - StreamSubscription _connectivity; - - @override - void initState() { - super.initState(); - _permissionReady = false; - _connectivity = Connectivity().onConnectivityChanged.listen((result) { - _usingData = result == ConnectivityResult.mobile; - }); - } - - @override - void dispose() { - _connectivity.cancel(); - super.dispose(); - } - - void _requestDownload(EpisodeBrief episode, bool downloadUsingData) async { - _permissionReady = await _checkPermmison(); - var _dataConfirm = true; - if (_permissionReady) { - if (downloadUsingData && _usingData) { - _dataConfirm = await _useDataConfirem(); + Future _requestDownload(EpisodeBrief episode) async { + final downloadUsingData = await KeyValueStorage(downloadUsingDataKey) + .getBool(defaultValue: true, reverse: true); + final permissionReady = await _checkPermmison(); + final result = await Connectivity().checkConnectivity(); + final usingData = result == ConnectivityResult.mobile; + var dataConfirm = true; + if (permissionReady) { + if (downloadUsingData && usingData) { + dataConfirm = await _useDataConfirm(); } - if (_dataConfirm) { + if (dataConfirm) { Provider.of(context, listen: false).startTask(episode); } } @@ -61,20 +47,20 @@ class _DownloadButtonState extends State { void _deleteDownload(EpisodeBrief episode) async { Provider.of(context, listen: false).delTask(episode); Fluttertoast.showToast( - msg: 'Download removed', + msg: context.s.downloadRemovedToast, gravity: ToastGravity.BOTTOM, ); } - _pauseDownload(EpisodeBrief episode) async { + Future _pauseDownload(EpisodeBrief episode) async { Provider.of(context, listen: false).pauseTask(episode); } - _resumeDownload(EpisodeBrief episode) async { + Future _resumeDownload(EpisodeBrief episode) async { Provider.of(context, listen: false).resumeTask(episode); } - _retryDownload(EpisodeBrief episode) async { + Future _retryDownload(EpisodeBrief episode) async { Provider.of(context, listen: false).retryTask(episode); } @@ -92,7 +78,7 @@ class _DownloadButtonState extends State { } } - Future _useDataConfirem() async { + Future _useDataConfirm() async { var ifUseData = false; final s = context.s; await generalDialog( @@ -164,24 +150,21 @@ class _DownloadButtonState extends State { Widget _downloadButton(EpisodeTask task, BuildContext context) { switch (task.status.value) { case 0: - return Selector( - selector: (_, settings) => settings.downloadUsingData, - builder: (_, data, __) => _buttonOnMenu( - Center( - child: SizedBox( - height: 20, - width: 20, - child: CustomPaint( - painter: DownloadPainter( - color: Colors.grey[700], - fraction: 0, - progressColor: context.accentColor, - ), + return _buttonOnMenu( + Center( + child: SizedBox( + height: 20, + width: 20, + child: CustomPaint( + painter: DownloadPainter( + color: Colors.grey[700], + fraction: 0, + progressColor: context.accentColor, ), ), ), - () => _requestDownload(task.episode, data)), - ); + ), + () => _requestDownload(task.episode)); break; case 2: return Material( diff --git a/lib/home/home_groups.dart b/lib/home/home_groups.dart index fe81144..e0f0734 100644 --- a/lib/home/home_groups.dart +++ b/lib/home/home_groups.dart @@ -1,6 +1,7 @@ import 'dart:async'; import 'dart:math' as math; +import 'package:connectivity/connectivity.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; @@ -8,7 +9,9 @@ import 'package:focused_menu/focused_menu.dart'; import 'package:focused_menu/modals.dart'; import 'package:intl/intl.dart'; import 'package:line_icons/line_icons.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; +import 'package:tsacdop/util/general_dialog.dart'; import 'package:tuple/tuple.dart'; import '../episodes/episode_detail.dart'; @@ -524,6 +527,7 @@ class PodcastPreview extends StatelessWidget { class ShowEpisode extends StatelessWidget { final List episodes; final PodcastLocal podcastLocal; + final DBHelper _dbHelper = DBHelper(); ShowEpisode({Key key, this.episodes, this.podcastLocal}) : super(key: key); String stringForSeconds(double seconds) { if (seconds == null) return null; @@ -557,8 +561,7 @@ class ShowEpisode extends StatelessWidget { } Future _isListened(EpisodeBrief episode) async { - var dbHelper = DBHelper(); - return await dbHelper.isListened(episode.enclosureUrl); + return await _dbHelper.isListened(episode.enclosureUrl); } Future _isLiked(EpisodeBrief episode) async { @@ -583,7 +586,7 @@ class ShowEpisode extends StatelessWidget { return boo == 1; } - _markListened(EpisodeBrief episode) async { + Future _markListened(EpisodeBrief episode) async { var dbHelper = DBHelper(); var marked = await dbHelper.checkMarked(episode); if (!marked) { @@ -592,16 +595,80 @@ class ShowEpisode extends StatelessWidget { } } - _saveLiked(String url) async { + Future _saveLiked(String url) async { var dbHelper = DBHelper(); await dbHelper.setLiked(url); } - _setUnliked(String url) async { + Future _setUnliked(String url) async { var dbHelper = DBHelper(); await dbHelper.setUniked(url); } + Future _requestDownload(BuildContext context, + {EpisodeBrief episode}) async { + final permissionReady = await _checkPermmison(); + final downloadUsingData = await KeyValueStorage(downloadUsingDataKey) + .getBool(defaultValue: true, reverse: true); + final result = await Connectivity().checkConnectivity(); + final usingData = result == ConnectivityResult.mobile; + var dataConfirm = true; + if (permissionReady) { + if (downloadUsingData && usingData) { + dataConfirm = await _useDataConfirm(context); + } + if (dataConfirm) { + Provider.of(context, listen: false).startTask(episode); + } + } + } + + Future _checkPermmison() async { + var permission = await Permission.storage.status; + if (permission != PermissionStatus.granted) { + var permissions = await [Permission.storage].request(); + if (permissions[Permission.storage] == PermissionStatus.granted) { + return true; + } else { + return false; + } + } else { + return true; + } + } + + Future _useDataConfirm(BuildContext context) async { + var ifUseData = false; + final s = context.s; + await generalDialog( + context, + title: Text(s.cellularConfirm), + content: Text(s.cellularConfirmDes), + actions: [ + FlatButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + s.cancel, + style: TextStyle(color: Colors.grey[600]), + ), + ), + FlatButton( + onPressed: () { + ifUseData = true; + Navigator.of(context).pop(); + }, + child: Text( + s.confirm, + style: TextStyle(color: Colors.red), + ), + ) + ], + ); + return ifUseData; + } + @override Widget build(BuildContext context) { var _width = context.width; @@ -637,11 +704,11 @@ class ShowEpisode extends StatelessWidget { future: _initData(episodes[index]), initialData: Tuple5(0, false, false, false, []), builder: (context, snapshot) { - var isListened = snapshot.data.item1; - var isLiked = snapshot.data.item2; - var isDownloaded = snapshot.data.item3; - var tapToOpen = snapshot.data.item4; - var menuList = snapshot.data.item5; + final isListened = snapshot.data.item1; + final isLiked = snapshot.data.item2; + final isDownloaded = snapshot.data.item3; + final tapToOpen = snapshot.data.item4; + final menuList = snapshot.data.item5; return Container( decoration: BoxDecoration( borderRadius: @@ -802,8 +869,10 @@ class ShowEpisode extends StatelessWidget { color: Colors.green), onPressed: () { if (!isDownloaded) { - downloader - .startTask(episodes[index]); + _requestDownload(context, + episode: episodes[index]); + // downloader + // .startTask(episodes[index]); } }) : null diff --git a/lib/podcasts/podcast_detail.dart b/lib/podcasts/podcast_detail.dart index 7a051ce..b39cfca 100644 --- a/lib/podcasts/podcast_detail.dart +++ b/lib/podcasts/podcast_detail.dart @@ -10,6 +10,7 @@ import 'package:flutter_linkify/flutter_linkify.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:html/parser.dart'; import 'package:line_icons/line_icons.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -656,7 +657,8 @@ class _PodcastDetailState extends State { width: 20, height: 10, child: CustomPaint( - painter: MultiSelectPainter(color: context.textColor)), + painter: + MultiSelectPainter(color: context.accentColor)), ), onPressed: () { setState(() { @@ -874,7 +876,9 @@ class _PodcastDetailState extends State { MultiSelectMenuBar( selectedList: _selectedEpisodes, onClose: (value) { - setState(() => _multiSelect = false); + setState(() { + if (value) _multiSelect = false; + }); }, ), SizedBox( @@ -913,6 +917,7 @@ class MultiSelectMenuBar extends StatefulWidget { class _MultiSelectMenuBarState extends State { bool _liked; bool _marked; + bool _inPlaylist; bool _downloaded; final _dbHelper = DBHelper(); @@ -922,11 +927,18 @@ class _MultiSelectMenuBarState extends State { _liked = false; _marked = false; _downloaded = false; + _inPlaylist = false; } @override void didUpdateWidget(MultiSelectMenuBar oldWidget) { if (oldWidget.selectedList != widget.selectedList) { + setState(() { + _liked = false; + _marked = false; + _downloaded = false; + _inPlaylist = false; + }); super.didUpdateWidget(oldWidget); } } @@ -935,14 +947,20 @@ class _MultiSelectMenuBarState extends State { for (var episode in widget.selectedList) { await _dbHelper.setLiked(episode.enclosureUrl); } - if (mounted) setState(() => _liked = true); + if (mounted) { + setState(() => _liked = true); + widget.onClose(false); + } } Future _setUnliked() async { for (var episode in widget.selectedList) { await _dbHelper.setUniked(episode.enclosureUrl); } - if (mounted) setState(() => _liked = false); + if (mounted) { + setState(() => _liked = false); + widget.onClose(false); + } } Future _markListened() async { @@ -950,14 +968,90 @@ class _MultiSelectMenuBarState extends State { final history = PlayHistory(episode.title, episode.enclosureUrl, 0, 1); await _dbHelper.saveHistory(history); } - if (mounted) setState(() => _marked = true); + if (mounted) { + setState(() => _marked = true); + widget.onClose(false); + } } Future _markNotListened() async { for (var episode in widget.selectedList) { await _dbHelper.markNotListened(episode.enclosureUrl); } - if (mounted) setState(() => _marked = false); + if (mounted) { + setState(() => _marked = false); + widget.onClose(false); + } + } + + Future _requestDownload() async { + final permissionReady = await _checkPermmison(); + final downloadUsingData = await KeyValueStorage(downloadUsingDataKey) + .getBool(defaultValue: true, reverse: true); + var dataConfirm = true; + final result = await Connectivity().checkConnectivity(); + final usingData = result == ConnectivityResult.mobile; + if (permissionReady) { + if (downloadUsingData && usingData) { + dataConfirm = await _useDataConfirm(); + } + if (dataConfirm) { + for (var episode in widget.selectedList) { + Provider.of(context, listen: false).startTask(episode); + } + if (mounted) { + setState(() { + _downloaded = true; + }); + } + } + } + } + + Future _useDataConfirm() async { + var ifUseData = false; + final s = context.s; + await generalDialog( + context, + title: Text(s.cellularConfirm), + content: Text(s.cellularConfirmDes), + actions: [ + FlatButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + s.cancel, + style: TextStyle(color: Colors.grey[600]), + ), + ), + FlatButton( + onPressed: () { + ifUseData = true; + Navigator.of(context).pop(); + }, + child: Text( + s.confirm, + style: TextStyle(color: Colors.red), + ), + ) + ], + ); + return ifUseData; + } + + Future _checkPermmison() async { + var permission = await Permission.storage.status; + if (permission != PermissionStatus.granted) { + var permissions = await [Permission.storage].request(); + if (permissions[Permission.storage] == PermissionStatus.granted) { + return true; + } else { + return false; + } + } else { + return true; + } } Widget _buttonOnMenu({Widget child, VoidCallback onTap}) => Material( @@ -989,55 +1083,145 @@ class _MultiSelectMenuBarState extends State { @override Widget build(BuildContext context) { - return Container( - height: 50.0, - decoration: BoxDecoration(color: context.primaryColor), - child: Row( - children: [ - _buttonOnMenu( - child: _liked - ? Icon(Icons.favorite, color: Colors.red) - : Icon( - Icons.favorite_border, - color: Colors.grey[700], + final s = context.s; + var audio = context.watch(); + return TweenAnimationBuilder( + tween: Tween(begin: 0, end: 1), + duration: Duration(milliseconds: 500), + builder: (context, value, child) => Container( + height: 50.0 * value, + decoration: BoxDecoration(color: context.primaryColor), + child: Row( + children: [ + _buttonOnMenu( + child: _liked + ? Icon(Icons.favorite, color: Colors.red) + : Icon( + Icons.favorite_border, + color: Colors.grey[700], + ), + onTap: () async { + if (widget.selectedList.isNotEmpty) { + if (!_liked) { + await _saveLiked(); + Fluttertoast.showToast( + msg: s.liked, + gravity: ToastGravity.BOTTOM, + ); + } else { + await _setUnliked(); + Fluttertoast.showToast( + msg: s.unliked, + gravity: ToastGravity.BOTTOM, + ); + } + } + // OverlayEntry _overlayEntry; + // _overlayEntry = _createOverlayEntry(); + // Overlay.of(context).insert(_overlayEntry); + // await Future.delayed(Duration(seconds: 2)); + // _overlayEntry?.remove(); + }), + _buttonOnMenu( + child: _downloaded + ? Center( + child: SizedBox( + height: 20, + width: 20, + child: CustomPaint( + painter: DownloadPainter( + color: context.accentColor, + fraction: 1, + progressColor: context.accentColor, + progress: 1), + ), + ), + ) + : Center( + child: SizedBox( + height: 20, + width: 20, + child: CustomPaint( + painter: DownloadPainter( + color: Colors.grey[700], + fraction: 0, + progressColor: context.accentColor, + ), + ), + ), ), - onTap: () async { - if (!_liked) { - await _saveLiked(); - } else { - await _setUnliked(); + onTap: () { + if (widget.selectedList.isNotEmpty) { + if (!_downloaded) _requestDownload(); } - // OverlayEntry _overlayEntry; - // _overlayEntry = _createOverlayEntry(); - // Overlay.of(context).insert(_overlayEntry); - // await Future.delayed(Duration(seconds: 2)); - // _overlayEntry?.remove(); - }), - _buttonOnMenu( - child: Padding( - padding: EdgeInsets.symmetric(vertical: 12), - child: CustomPaint( - size: Size(25, 20), - painter: ListenedAllPainter( - _marked ? context.accentColor : Colors.grey[700], - stroke: 2.0), + }, + ), + _buttonOnMenu( + child: _inPlaylist + ? Icon(Icons.playlist_add_check, color: context.accentColor) + : Icon( + Icons.playlist_add, + color: Colors.grey[700], + ), + onTap: () async { + if (widget.selectedList.isNotEmpty) { + if (!_inPlaylist) { + for (var episode in widget.selectedList) { + audio.addToPlaylist(episode); + Fluttertoast.showToast( + msg: s.toastAddPlaylist, + gravity: ToastGravity.BOTTOM, + ); + } + setState(() => _inPlaylist = true); + } else { + for (var episode in widget.selectedList) { + audio.delFromPlaylist(episode); + Fluttertoast.showToast( + msg: s.toastRemovePlaylist, + gravity: ToastGravity.BOTTOM, + ); + } + setState(() => _inPlaylist = false); + } + } + }), + _buttonOnMenu( + child: Padding( + padding: EdgeInsets.symmetric(vertical: 12), + child: CustomPaint( + size: Size(25, 20), + painter: ListenedAllPainter( + _marked ? context.accentColor : Colors.grey[700], + stroke: 2.0), + ), ), - ), - onTap: () async { - if (!_marked) { - await _markListened(); - } else { - await _markNotListened(); - } - }), - Spacer(), - Padding( - padding: EdgeInsets.symmetric(horizontal: 10.0), - child: Text('${widget.selectedList.length} selected', - style: context.textTheme.headline6)), - _buttonOnMenu( - child: Icon(Icons.close), onTap: () => widget.onClose(true)) - ], + onTap: () async { + if (widget.selectedList.isNotEmpty) { + if (!_marked) { + await _markListened(); + Fluttertoast.showToast( + msg: s.markListened, + gravity: ToastGravity.BOTTOM, + ); + } else { + await _markNotListened(); + Fluttertoast.showToast( + msg: s.markNotListened, + gravity: ToastGravity.BOTTOM, + ); + } + } + }), + Spacer(), + Padding( + padding: EdgeInsets.symmetric(horizontal: 10.0), + child: Text('${widget.selectedList.length} selected', + style: context.textTheme.headline6)), + _buttonOnMenu( + child: Icon(Icons.close), onTap: () => widget.onClose(true)) + ], + ), ), ); } diff --git a/lib/state/download_state.dart b/lib/state/download_state.dart index ded19a4..8fd628e 100644 --- a/lib/state/download_state.dart +++ b/lib/state/download_state.dart @@ -258,7 +258,7 @@ class DownloadState extends ChangeNotifier { if (!isDownloaded) { final dir = await getExternalStorageDirectory(); var localPath = - path.join(dir.path, episode.feedTitle.replaceAll('/', '')); + path.join(dir.path, episode.feedTitle?.replaceAll('/', '')); final saveDir = Directory(localPath); var hasExisted = await saveDir.exists(); if (!hasExisted) { diff --git a/lib/util/custom_widget.dart b/lib/util/custom_widget.dart index 3e066e3..434eb87 100644 --- a/lib/util/custom_widget.dart +++ b/lib/util/custom_widget.dart @@ -47,7 +47,7 @@ class MultiSelectPainter extends CustomPainter { var paint = Paint() ..color = color ..strokeWidth = 1.0 - ..style = PaintingStyle.stroke + ..style = PaintingStyle.fill ..strokeCap = StrokeCap.round; final x = size.width / 2; final y = size.height / 2; @@ -59,6 +59,7 @@ class MultiSelectPainter extends CustomPainter { path.lineTo(x * 2, y); path.lineTo(0, y); path.lineTo(0, 0); + path.close(); canvas.drawPath(path, paint); } diff --git a/lib/util/episodegrid.dart b/lib/util/episodegrid.dart index 3f61f25..a898334 100644 --- a/lib/util/episodegrid.dart +++ b/lib/util/episodegrid.dart @@ -1,12 +1,14 @@ import 'dart:ui'; import 'package:auto_animated/auto_animated.dart'; +import 'package:connectivity/connectivity.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:focused_menu/focused_menu.dart'; import 'package:focused_menu/modals.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:line_icons/line_icons.dart'; +import 'package:permission_handler/permission_handler.dart'; import 'package:provider/provider.dart'; import 'package:tuple/tuple.dart'; @@ -20,6 +22,7 @@ import '../type/episodebrief.dart'; import '../type/play_histroy.dart'; import 'custom_widget.dart'; import 'extension_helper.dart'; +import 'general_dialog.dart'; import 'open_container.dart'; enum Layout { three, two, one } @@ -115,6 +118,70 @@ class EpisodeGrid extends StatelessWidget { await dbHelper.setUniked(url); } + Future _requestDownload(BuildContext context, + {EpisodeBrief episode}) async { + final permissionReady = await _checkPermmison(); + final downloadUsingData = await KeyValueStorage(downloadUsingDataKey) + .getBool(defaultValue: true, reverse: true); + final result = await Connectivity().checkConnectivity(); + final usingData = result == ConnectivityResult.mobile; + var dataConfirm = true; + if (permissionReady) { + if (downloadUsingData && usingData) { + dataConfirm = await _useDataConfirm(context); + } + if (dataConfirm) { + context.read().startTask(episode); + } + } + } + + Future _checkPermmison() async { + var permission = await Permission.storage.status; + if (permission != PermissionStatus.granted) { + var permissions = await [Permission.storage].request(); + if (permissions[Permission.storage] == PermissionStatus.granted) { + return true; + } else { + return false; + } + } else { + return true; + } + } + + Future _useDataConfirm(BuildContext context) async { + var ifUseData = false; + final s = context.s; + await generalDialog( + context, + title: Text(s.cellularConfirm), + content: Text(s.cellularConfirmDes), + actions: [ + FlatButton( + onPressed: () { + Navigator.of(context).pop(); + }, + child: Text( + s.cancel, + style: TextStyle(color: Colors.grey[600]), + ), + ), + FlatButton( + onPressed: () { + ifUseData = true; + Navigator.of(context).pop(); + }, + child: Text( + s.confirm, + style: TextStyle(color: Colors.red), + ), + ) + ], + ); + return ifUseData; + } + /// Episode title widget. Widget _title(EpisodeBrief episode) => Container( alignment: @@ -594,10 +661,12 @@ class EpisodeGrid extends StatelessWidget { trailingIcon: Icon( LineIcons.download_solid, color: Colors.green), - onPressed: () { + onPressed: () async { if (!isDownloaded) { - downloader - .startTask(episodes[index]); + await _requestDownload(context, + episode: episodes[index]); + // downloader + // .startTask(episodes[index]); } }) : null