Improve multi select feature.

This commit is contained in:
stonegate 2020-09-15 19:48:22 +08:00
parent cd1a422f73
commit 9fdd549d5c
6 changed files with 421 additions and 115 deletions

View File

@ -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<DownloadButton> {
bool _permissionReady;
bool _usingData;
StreamSubscription _connectivity;
@override
void initState() {
super.initState();
_permissionReady = false;
_connectivity = Connectivity().onConnectivityChanged.listen((result) {
_usingData = result == ConnectivityResult.mobile;
});
Future<void> _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();
}
@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();
}
if (_dataConfirm) {
if (dataConfirm) {
Provider.of<DownloadState>(context, listen: false).startTask(episode);
}
}
@ -61,20 +47,20 @@ class _DownloadButtonState extends State<DownloadButton> {
void _deleteDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).delTask(episode);
Fluttertoast.showToast(
msg: 'Download removed',
msg: context.s.downloadRemovedToast,
gravity: ToastGravity.BOTTOM,
);
}
_pauseDownload(EpisodeBrief episode) async {
Future<void> _pauseDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).pauseTask(episode);
}
_resumeDownload(EpisodeBrief episode) async {
Future<void> _resumeDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).resumeTask(episode);
}
_retryDownload(EpisodeBrief episode) async {
Future<void> _retryDownload(EpisodeBrief episode) async {
Provider.of<DownloadState>(context, listen: false).retryTask(episode);
}
@ -92,7 +78,7 @@ class _DownloadButtonState extends State<DownloadButton> {
}
}
Future<bool> _useDataConfirem() async {
Future<bool> _useDataConfirm() async {
var ifUseData = false;
final s = context.s;
await generalDialog(
@ -164,9 +150,7 @@ class _DownloadButtonState extends State<DownloadButton> {
Widget _downloadButton(EpisodeTask task, BuildContext context) {
switch (task.status.value) {
case 0:
return Selector<SettingState, bool>(
selector: (_, settings) => settings.downloadUsingData,
builder: (_, data, __) => _buttonOnMenu(
return _buttonOnMenu(
Center(
child: SizedBox(
height: 20,
@ -180,8 +164,7 @@ class _DownloadButtonState extends State<DownloadButton> {
),
),
),
() => _requestDownload(task.episode, data)),
);
() => _requestDownload(task.episode));
break;
case 2:
return Material(

View File

@ -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<EpisodeBrief> 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<int> _isListened(EpisodeBrief episode) async {
var dbHelper = DBHelper();
return await dbHelper.isListened(episode.enclosureUrl);
return await _dbHelper.isListened(episode.enclosureUrl);
}
Future<bool> _isLiked(EpisodeBrief episode) async {
@ -583,7 +586,7 @@ class ShowEpisode extends StatelessWidget {
return boo == 1;
}
_markListened(EpisodeBrief episode) async {
Future<void> _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<void> _saveLiked(String url) async {
var dbHelper = DBHelper();
await dbHelper.setLiked(url);
}
_setUnliked(String url) async {
Future<void> _setUnliked(String url) async {
var dbHelper = DBHelper();
await dbHelper.setUniked(url);
}
Future<void> _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<DownloadState>(context, listen: false).startTask(episode);
}
}
}
Future<bool> _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<bool> _useDataConfirm(BuildContext context) async {
var ifUseData = false;
final s = context.s;
await generalDialog(
context,
title: Text(s.cellularConfirm),
content: Text(s.cellularConfirmDes),
actions: <Widget>[
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

View File

@ -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<PodcastDetail> {
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<PodcastDetail> {
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<MultiSelectMenuBar> {
bool _liked;
bool _marked;
bool _inPlaylist;
bool _downloaded;
final _dbHelper = DBHelper();
@ -922,11 +927,18 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
_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<MultiSelectMenuBar> {
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<void> _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<void> _markListened() async {
@ -950,14 +968,90 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
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<void> _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<void> _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<DownloadState>(context, listen: false).startTask(episode);
}
if (mounted) {
setState(() {
_downloaded = true;
});
}
}
}
}
Future<bool> _useDataConfirm() async {
var ifUseData = false;
final s = context.s;
await generalDialog(
context,
title: Text(s.cellularConfirm),
content: Text(s.cellularConfirmDes),
actions: <Widget>[
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<bool> _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,8 +1083,13 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
@override
Widget build(BuildContext context) {
return Container(
height: 50.0,
final s = context.s;
var audio = context.watch<AudioPlayerNotifier>();
return TweenAnimationBuilder(
tween: Tween<double>(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: [
@ -1002,10 +1101,20 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
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();
@ -1013,6 +1122,70 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
// 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: () {
if (widget.selectedList.isNotEmpty) {
if (!_downloaded) _requestDownload();
}
},
),
_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),
@ -1024,10 +1197,20 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
),
),
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(),
@ -1039,6 +1222,7 @@ class _MultiSelectMenuBarState extends State<MultiSelectMenuBar> {
child: Icon(Icons.close), onTap: () => widget.onClose(true))
],
),
),
);
}
}

View File

@ -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) {

View File

@ -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);
}

View File

@ -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<void> _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<DownloadState>().startTask(episode);
}
}
}
Future<bool> _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<bool> _useDataConfirm(BuildContext context) async {
var ifUseData = false;
final s = context.s;
await generalDialog(
context,
title: Text(s.cellularConfirm),
content: Text(s.cellularConfirmDes),
actions: <Widget>[
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