371 lines
11 KiB
Dart
371 lines
11 KiB
Dart
import 'dart:io';
|
|
|
|
import 'package:filesize/filesize.dart';
|
|
import 'package:flutter/material.dart';
|
|
import 'package:freezer/api/download.dart';
|
|
import 'package:freezer/translations.i18n.dart';
|
|
import 'package:freezer/ui/elements.dart';
|
|
import 'package:path/path.dart' as p;
|
|
import 'package:path_provider/path_provider.dart';
|
|
import 'cached_image.dart';
|
|
|
|
import 'dart:async';
|
|
|
|
|
|
class DownloadsScreen extends StatefulWidget {
|
|
@override
|
|
_DownloadsScreenState createState() => _DownloadsScreenState();
|
|
}
|
|
|
|
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(
|
|
appBar: FreezerAppBar(
|
|
'Downloads'.i18n,
|
|
actions: [
|
|
IconButton(
|
|
icon: Icon(Icons.delete_sweep),
|
|
onPressed: () async {
|
|
await downloadManager.removeDownloads(DownloadState.ERROR);
|
|
await downloadManager.removeDownloads(DownloadState.DEEZER_ERROR);
|
|
await downloadManager.removeDownloads(DownloadState.DONE);
|
|
await _load();
|
|
},
|
|
),
|
|
IconButton(
|
|
icon:
|
|
Icon(downloadManager.running ? Icons.stop : Icons.play_arrow),
|
|
onPressed: () {
|
|
setState(() {
|
|
if (downloadManager.running)
|
|
downloadManager.stop();
|
|
else
|
|
downloadManager.start();
|
|
});
|
|
},
|
|
)
|
|
],
|
|
),
|
|
body: ListView(
|
|
children: [
|
|
//Now downloading
|
|
Container(height: 2.0),
|
|
Column(children: List.generate(downloading.length, (int i) => DownloadTile(
|
|
downloading[i],
|
|
updateCallback: () => _load(),
|
|
))),
|
|
Container(height: 8.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();
|
|
},
|
|
),
|
|
|
|
//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();
|
|
},
|
|
),
|
|
|
|
],
|
|
)
|
|
);
|
|
}
|
|
}
|
|
|
|
class DownloadTile extends StatelessWidget {
|
|
|
|
final Download download;
|
|
final Function updateCallback;
|
|
DownloadTile(this.download, {this.updateCallback});
|
|
|
|
String subtitle() {
|
|
String out = '';
|
|
|
|
if (download.state != DownloadState.DOWNLOADING && download.state != DownloadState.POST) {
|
|
//Download type
|
|
if (download.private) out += 'Offline'.i18n;
|
|
else out += 'External'.i18n;
|
|
out += ' | ';
|
|
}
|
|
|
|
if (download.state == DownloadState.POST) {
|
|
return 'Post processing...'.i18n;
|
|
}
|
|
|
|
//Quality
|
|
if (download.quality == 9) out += 'FLAC';
|
|
if (download.quality == 3) out += 'MP3 320kbps';
|
|
if (download.quality == 1) out += 'MP3 128kbps';
|
|
|
|
//Downloading show progress
|
|
if (download.state == DownloadState.DOWNLOADING) {
|
|
out += ' | ${filesize(download.received, 2)} / ${filesize(download.filesize, 2)}';
|
|
double progress = download.received.toDouble() / download.filesize.toDouble();
|
|
out += ' ${(progress*100.0).toStringAsFixed(2)}%';
|
|
}
|
|
|
|
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(), maxLines: 1, overflow: TextOverflow.ellipsis),
|
|
trailing: trailing(),
|
|
onTap: () => onClick(context),
|
|
),
|
|
if (download.state == DownloadState.DOWNLOADING)
|
|
LinearProgressIndicator(value: download.progress),
|
|
if (download.state == DownloadState.POST)
|
|
LinearProgressIndicator(),
|
|
],
|
|
);
|
|
}
|
|
}
|
|
|
|
class DownloadLogViewer extends StatefulWidget {
|
|
@override
|
|
_DownloadLogViewerState createState() => _DownloadLogViewerState();
|
|
}
|
|
|
|
class _DownloadLogViewerState extends State<DownloadLogViewer> {
|
|
|
|
List<String> data = [];
|
|
|
|
//Load log from file
|
|
Future _load() async {
|
|
String path = p.join((await getExternalStorageDirectory()).path, 'download.log');
|
|
File file = File(path);
|
|
if (await file.exists()) {
|
|
String _d = await file.readAsString();
|
|
setState(() {
|
|
data = _d.replaceAll("\r", "").split("\n");
|
|
});
|
|
}
|
|
}
|
|
|
|
//Get color by log type
|
|
Color color(String line) {
|
|
if (line.startsWith('E:')) return Colors.red;
|
|
if (line.startsWith('W:')) return Colors.orange[600];
|
|
return null;
|
|
}
|
|
|
|
@override
|
|
void initState() {
|
|
_load();
|
|
super.initState();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Scaffold(
|
|
appBar: FreezerAppBar('Download Log'.i18n),
|
|
body: ListView.builder(
|
|
itemCount: data.length,
|
|
itemBuilder: (context, i) {
|
|
return Padding(
|
|
padding: EdgeInsets.all(8.0),
|
|
child: Text(
|
|
data[i],
|
|
style: TextStyle(
|
|
fontSize: 14.0,
|
|
color: color(data[i])
|
|
),
|
|
),
|
|
);
|
|
},
|
|
)
|
|
);
|
|
}
|
|
}
|