0.5.0 - Rewritten downloads, many bugfixes

This commit is contained in:
exttex 2020-10-09 20:52:45 +02:00
parent f7cbb09bc1
commit f2f6b202d1
38 changed files with 5176 additions and 1365 deletions

View file

@ -1,99 +1,11 @@
import 'dart:async';
import 'package:filesize/filesize.dart';
import 'package:flutter/material.dart';
import 'package:freezer/api/download.dart';
import 'package:freezer/translations.i18n.dart';
import 'cached_image.dart';
import '../api/download.dart';
class DownloadTile extends StatelessWidget {
final Download download;
Function onDelete;
DownloadTile(this.download, {this.onDelete});
String get subtitle {
switch (download.state) {
case DownloadState.NONE: return '';
case DownloadState.DOWNLOADING:
return '${filesize(download.received)} / ${filesize(download.total)}';
case DownloadState.POST:
return 'Post processing...'.i18n;
case DownloadState.DONE:
return 'Done'.i18n; //Shouldn't be visible
case DownloadState.DEEZER_ERROR:
return 'Track is not available on Deezer!'.i18n;
case DownloadState.ERROR:
return 'Failed to download track! Please restart.'.i18n;
}
return '';
}
Widget get progressBar {
switch (download.state) {
case DownloadState.DOWNLOADING:
return LinearProgressIndicator(value: download.received / download.total);
case DownloadState.POST:
return LinearProgressIndicator();
default:
return Container(height: 0, width: 0,);
}
}
Widget get trailing {
if (download.private) {
return Icon(Icons.offline_pin);
}
return Icon(Icons.sd_card);
}
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: <Widget>[
ListTile(
title: Text(download.track.title),
subtitle: Text(subtitle),
leading: CachedImage(
url: download.track.albumArt.thumb,
width: 48.0,
),
trailing: trailing,
onTap: () {
//Delete if none
if (download.state == DownloadState.NONE) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Delete'.i18n),
content: Text('Are you sure you want to delete this download?'.i18n),
actions: [
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Delete'.i18n),
onPressed: () {
downloadManager.removeDownload(download);
if (this.onDelete != null) this.onDelete();
Navigator.of(context).pop();
},
)
],
);
}
);
}
},
),
progressBar
],
);
}
}
class DownloadsScreen extends StatefulWidget {
@override
@ -101,6 +13,55 @@ class DownloadsScreen extends StatefulWidget {
}
class _DownloadsScreenState extends State<DownloadsScreen> {
List<Download> downloads = [];
StreamSubscription _stateSubscription;
//Sublists
List<Download> get downloading => downloads.where((d) => d.state == DownloadState.DOWNLOADING || d.state == DownloadState.POST).toList();
List<Download> get queued => downloads.where((d) => d.state == DownloadState.NONE).toList();
List<Download> get failed => downloads.where((d) => d.state == DownloadState.ERROR || d.state == DownloadState.DEEZER_ERROR).toList();
List<Download> get finished => downloads.where((d) => d.state == DownloadState.DONE).toList();
Future _load() async {
//Load downloads
List<Download> _d = await downloadManager.getDownloads();
setState(() {
downloads = _d;
});
}
@override
void initState() {
_load();
//Subscribe to state update
_stateSubscription = downloadManager.serviceEvents.stream.listen((e) {
//State change = update
if (e['action'] == 'onStateChange') {
setState(() => downloadManager.running = downloadManager.running);
}
//Progress change
if (e['action'] == 'onProgress') {
setState(() {
for (Map su in e['data']) {
downloads.firstWhere((d) => d.id == su['id'], orElse: () => Download()).updateFromJson(su);
}
});
}
});
super.initState();
}
@override
void dispose() {
if (_stateSubscription != null)
_stateSubscription.cancel();
_stateSubscription = null;
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -108,100 +69,216 @@ class _DownloadsScreenState extends State<DownloadsScreen> {
title: Text('Downloads'.i18n),
actions: [
IconButton(
icon: Icon(downloadManager.stopped ? Icons.play_arrow : Icons.stop),
icon:
Icon(downloadManager.running ? Icons.stop : Icons.play_arrow),
onPressed: () {
setState(() {
if (downloadManager.stopped) downloadManager.start();
else downloadManager.stop();
if (downloadManager.running)
downloadManager.stop();
else
downloadManager.start();
});
},
)
],
),
body: ListView(
children: <Widget>[
StreamBuilder(
stream: Stream.periodic(Duration(milliseconds: 500)).asBroadcastStream(), //Periodic to get current download progress
builder: (BuildContext context, AsyncSnapshot snapshot) {
children: [
//Now downloading
Container(height: 2.0),
Column(children: List.generate(downloading.length, (int i) => DownloadTile(
downloading[i],
updateCallback: () => _load(),
))),
Container(height: 8.0),
if (downloadManager.queue.length == 0)
return Container(width: 0, height: 0,);
//Queued
if (queued.length > 0)
Text(
'Queued'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold
),
),
Column(children: List.generate(queued.length, (int i) => DownloadTile(
queued[i],
updateCallback: () => _load(),
))),
if (queued.length > 0)
ListTile(
title: Text('Clear queue'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await downloadManager.removeDownloads(DownloadState.NONE);
await _load();
},
),
return Column(
children: [
...List.generate(downloadManager.queue.length, (i) {
return DownloadTile(downloadManager.queue[i], onDelete: () => setState(() => {}));
}),
if (downloadManager.queue.length > 1 || (downloadManager.stopped && downloadManager.queue.length > 0))
ListTile(
title: Text('Clear queue'.i18n),
subtitle: Text("This won't delete currently downloading item".i18n),
leading: Icon(Icons.delete),
onTap: () async {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Delete'.i18n),
content: Text('Are you sure you want to delete all queued downloads?'.i18n),
actions: [
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Delete'.i18n),
onPressed: () async {
await downloadManager.clearQueue();
Navigator.of(context).pop();
},
)
],
);
}
);
},
)
]
);
},
),
FutureBuilder(
future: downloadManager.getFinishedDownloads(),
builder: (context, snapshot) {
if (!snapshot.hasData || snapshot.data.length == 0) return Container(height: 0, width: 0,);
//Failed
if (failed.length > 0)
Text(
'Failed'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold
),
),
Column(children: List.generate(failed.length, (int i) => DownloadTile(
failed[i],
updateCallback: () => _load(),
))),
//Restart failed
if (failed.length > 0)
ListTile(
title: Text('Restart failed downloads'.i18n),
leading: Icon(Icons.restore),
onTap: () async {
await downloadManager.retryDownloads();
await _load();
},
),
if (failed.length > 0)
ListTile(
title: Text('Clear failed'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await downloadManager.removeDownloads(DownloadState.ERROR);
await downloadManager.removeDownloads(DownloadState.DEEZER_ERROR);
await _load();
},
),
//Finished
if (finished.length > 0)
Text(
'Done'.i18n,
textAlign: TextAlign.center,
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold
),
),
Column(children: List.generate(finished.length, (int i) => DownloadTile(
finished[i],
updateCallback: () => _load(),
))),
if (finished.length > 0)
ListTile(
title: Text('Clear downloads history'.i18n),
leading: Icon(Icons.delete),
onTap: () async {
await downloadManager.removeDownloads(DownloadState.DONE);
await _load();
},
),
return Column(
children: <Widget>[
Divider(),
Text(
'History',
style: TextStyle(
fontSize: 24.0,
fontWeight: FontWeight.bold
),
),
...List.generate(snapshot.data.length, (i) {
Download d = snapshot.data[i];
return DownloadTile(d);
}),
ListTile(
title: Text('Clear downloads history'.i18n),
leading: Icon(Icons.delete),
subtitle: Text('WARNING: This will only clear non-offline (external downloads)'.i18n),
onTap: () async {
await downloadManager.cleanDownloadHistory();
setState(() {});
},
),
],
);
},
)
],
)
);
}
}
class DownloadTile extends StatelessWidget {
final Download download;
final Function updateCallback;
DownloadTile(this.download, {this.updateCallback});
String subtitle() {
String out = '';
//Download type
if (download.private) out += 'Offline'.i18n;
else out += 'External'.i18n;
out += ' | ';
//Quality
if (download.quality == 9) out += 'FLAC';
if (download.quality == 3) out += 'MP3 320kbps';
if (download.quality == 1) out += 'MP3 128kbps';
return out;
}
Future onClick(BuildContext context) async {
if (download.state != DownloadState.DOWNLOADING && download.state != DownloadState.POST) {
showDialog(
context: context,
builder: (context) {
return AlertDialog(
title: Text('Delete'.i18n),
content: Text('Are you sure you want to delete this download?'.i18n),
actions: [
FlatButton(
child: Text('Cancel'.i18n),
onPressed: () => Navigator.of(context).pop(),
),
FlatButton(
child: Text('Delete'.i18n),
onPressed: () async {
await downloadManager.removeDownload(download.id);
if (updateCallback != null) updateCallback();
Navigator.of(context).pop();
},
)
],
);
}
);
}
}
//Trailing icon with state
Widget trailing() {
switch (download.state) {
case DownloadState.NONE:
return Icon(
Icons.query_builder,
);
case DownloadState.DOWNLOADING:
return Icon(
Icons.download_rounded
);
case DownloadState.POST:
return Icon(
Icons.miscellaneous_services
);
case DownloadState.DONE:
return Icon(
Icons.done,
color: Colors.green,
);
case DownloadState.DEEZER_ERROR:
return Icon(
Icons.error,
color: Colors.blue
);
case DownloadState.ERROR:
return Icon(
Icons.error,
color: Colors.red
);
}
return Container();
}
@override
Widget build(BuildContext context) {
return Column(
children: [
ListTile(
title: Text(download.title),
leading: CachedImage(url: download.image),
subtitle: Text(subtitle()),
trailing: trailing(),
onTap: () => onClick(context),
),
if (download.state == DownloadState.DOWNLOADING)
LinearProgressIndicator(value: download.progress),
if (download.state == DownloadState.POST)
LinearProgressIndicator(),
],
);
}
}