0.5.0 - Rewritten downloads, many bugfixes
This commit is contained in:
parent
f7cbb09bc1
commit
f2f6b202d1
38 changed files with 5176 additions and 1365 deletions
|
|
@ -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(),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue