mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feat: Add download speed to progress bar
This commit is contained in:
parent
d22d340181
commit
5a5a4e4703
7 changed files with 95 additions and 68 deletions
|
|
@ -2,5 +2,4 @@ arb-dir: lib/l10n
|
||||||
template-arb-file: app_en.arb
|
template-arb-file: app_en.arb
|
||||||
output-localization-file: app_localizations.dart
|
output-localization-file: app_localizations.dart
|
||||||
nullable-getter: false
|
nullable-getter: false
|
||||||
synthetic-package: false
|
|
||||||
output-dir: lib/l10n/generated
|
output-dir: lib/l10n/generated
|
||||||
|
|
|
||||||
|
|
@ -4,11 +4,13 @@ class DownloadStream {
|
||||||
final String id;
|
final String id;
|
||||||
final dl.DownloadTask? task;
|
final dl.DownloadTask? task;
|
||||||
final double progress;
|
final double progress;
|
||||||
|
final String downloadSpeed;
|
||||||
final dl.TaskStatus status;
|
final dl.TaskStatus status;
|
||||||
DownloadStream({
|
DownloadStream({
|
||||||
required this.id,
|
required this.id,
|
||||||
this.task,
|
this.task,
|
||||||
required this.progress,
|
this.progress = -1,
|
||||||
|
this.downloadSpeed = "",
|
||||||
required this.status,
|
required this.status,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -16,6 +18,7 @@ class DownloadStream {
|
||||||
: id = '',
|
: id = '',
|
||||||
task = null,
|
task = null,
|
||||||
progress = -1,
|
progress = -1,
|
||||||
|
downloadSpeed = "",
|
||||||
status = dl.TaskStatus.notFound;
|
status = dl.TaskStatus.notFound;
|
||||||
|
|
||||||
bool get hasDownload => progress != -1.0 && status != dl.TaskStatus.notFound && status != dl.TaskStatus.complete;
|
bool get hasDownload => progress != -1.0 && status != dl.TaskStatus.notFound && status != dl.TaskStatus.complete;
|
||||||
|
|
@ -24,12 +27,14 @@ class DownloadStream {
|
||||||
String? id,
|
String? id,
|
||||||
dl.DownloadTask? task,
|
dl.DownloadTask? task,
|
||||||
double? progress,
|
double? progress,
|
||||||
|
String? downloadSpeed,
|
||||||
dl.TaskStatus? status,
|
dl.TaskStatus? status,
|
||||||
}) {
|
}) {
|
||||||
return DownloadStream(
|
return DownloadStream(
|
||||||
id: id ?? this.id,
|
id: id ?? this.id,
|
||||||
task: task ?? this.task,
|
task: task ?? this.task,
|
||||||
progress: progress ?? this.progress,
|
progress: progress ?? this.progress,
|
||||||
|
downloadSpeed: downloadSpeed ?? this.downloadSpeed,
|
||||||
status: status ?? this.status,
|
status: status ?? this.status,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -82,10 +82,6 @@ abstract class SyncedItem with _$SyncedItem {
|
||||||
_ => TaskStatus.notFound,
|
_ => TaskStatus.notFound,
|
||||||
};
|
};
|
||||||
|
|
||||||
String? get taskId => task?.taskId;
|
|
||||||
|
|
||||||
bool get childHasTask => false;
|
|
||||||
|
|
||||||
double get totalProgress => 0.0;
|
double get totalProgress => 0.0;
|
||||||
|
|
||||||
bool get hasVideoFile => videoFileName?.isNotEmpty == true && (fileSize ?? 0) > 0;
|
bool get hasVideoFile => videoFileName?.isNotEmpty == true && (fileSize ?? 0) > 0;
|
||||||
|
|
@ -94,10 +90,6 @@ abstract class SyncedItem with _$SyncedItem {
|
||||||
return TaskStatus.notFound;
|
return TaskStatus.notFound;
|
||||||
}
|
}
|
||||||
|
|
||||||
double get downloadProgress => 0.0;
|
|
||||||
TaskStatus get downloadStatus => TaskStatus.notFound;
|
|
||||||
DownloadTask? get task => null;
|
|
||||||
|
|
||||||
Future<bool> deleteDatFiles(Ref ref) async {
|
Future<bool> deleteDatFiles(Ref ref) async {
|
||||||
try {
|
try {
|
||||||
await videoFile.delete();
|
await videoFile.delete();
|
||||||
|
|
|
||||||
|
|
@ -1,23 +1,59 @@
|
||||||
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:flutter/widgets.dart';
|
import 'package:flutter/widgets.dart';
|
||||||
|
|
||||||
import 'package:background_downloader/background_downloader.dart';
|
import 'package:background_downloader/background_downloader.dart';
|
||||||
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
import 'package:riverpod_annotation/riverpod_annotation.dart';
|
||||||
|
|
||||||
|
import 'package:fladder/models/syncing/download_stream.dart';
|
||||||
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
import 'package:fladder/providers/settings/client_settings_provider.dart';
|
||||||
|
import 'package:fladder/providers/sync_provider.dart';
|
||||||
import 'package:fladder/util/localization_helper.dart';
|
import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
part 'background_download_provider.g.dart';
|
part 'background_download_provider.g.dart';
|
||||||
|
|
||||||
|
final itemDownloadGroup = "ITEM_DOWNLOAD_GROUP";
|
||||||
|
|
||||||
@Riverpod(keepAlive: true)
|
@Riverpod(keepAlive: true)
|
||||||
class BackgroundDownloader extends _$BackgroundDownloader {
|
class BackgroundDownloader extends _$BackgroundDownloader {
|
||||||
|
late StreamSubscription<TaskUpdate> updateListener;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
FileDownloader build() {
|
FileDownloader build() {
|
||||||
|
ref.onDispose(
|
||||||
|
() => updateListener.cancel(),
|
||||||
|
);
|
||||||
|
|
||||||
final maxDownloads = ref.read(clientSettingsProvider.select((value) => value.maxConcurrentDownloads));
|
final maxDownloads = ref.read(clientSettingsProvider.select((value) => value.maxConcurrentDownloads));
|
||||||
return FileDownloader()
|
final downloader = FileDownloader()
|
||||||
..configure(
|
..configure(
|
||||||
globalConfig: globalConfig(maxDownloads),
|
globalConfig: globalConfig(maxDownloads),
|
||||||
)
|
)
|
||||||
..trackTasks();
|
..trackTasks();
|
||||||
|
updateListener = downloader.updates.listen(updateTask);
|
||||||
|
return downloader;
|
||||||
|
}
|
||||||
|
|
||||||
|
void updateTask(TaskUpdate update) {
|
||||||
|
switch (update) {
|
||||||
|
case TaskStatusUpdate():
|
||||||
|
final status = update.status;
|
||||||
|
ref.read(downloadTasksProvider(update.task.taskId).notifier).update(
|
||||||
|
(state) => state.copyWith(status: status),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (status == TaskStatus.complete || status == TaskStatus.canceled) {
|
||||||
|
ref.read(downloadTasksProvider(update.task.taskId).notifier).update((state) => DownloadStream.empty());
|
||||||
|
}
|
||||||
|
case TaskProgressUpdate():
|
||||||
|
final progress = update.progress;
|
||||||
|
ref.read(downloadTasksProvider(update.task.taskId).notifier).update(
|
||||||
|
(state) => state.copyWith(
|
||||||
|
progress: progress > 0 && progress < 1 ? progress : null,
|
||||||
|
downloadSpeed: update.networkSpeedAsString,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setMaxConcurrent(int value) {
|
void setMaxConcurrent(int value) {
|
||||||
|
|
|
||||||
|
|
@ -44,7 +44,7 @@ import 'package:fladder/util/localization_helper.dart';
|
||||||
|
|
||||||
final syncProvider = StateNotifierProvider<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError());
|
final syncProvider = StateNotifierProvider<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError());
|
||||||
|
|
||||||
final downloadTasksProvider = StateProvider.family<DownloadStream, String>((ref, id) => DownloadStream.empty());
|
final downloadTasksProvider = StateProvider.family<DownloadStream, String?>((ref, id) => DownloadStream.empty());
|
||||||
|
|
||||||
class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||||
SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) {
|
SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) {
|
||||||
|
|
@ -315,17 +315,13 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||||
)
|
)
|
||||||
.toList());
|
.toList());
|
||||||
|
|
||||||
if (item.taskId != null) {
|
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.id);
|
||||||
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!);
|
|
||||||
}
|
|
||||||
|
|
||||||
await _db.deleteAllItems([...nestedChildren, item]);
|
await _db.deleteAllItems([...nestedChildren, item]);
|
||||||
|
|
||||||
for (var i = 0; i < nestedChildren.length; i++) {
|
for (var i = 0; i < nestedChildren.length; i++) {
|
||||||
final element = nestedChildren[i];
|
final element = nestedChildren[i];
|
||||||
if (element.taskId != null) {
|
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(element.id);
|
||||||
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(element.taskId!);
|
|
||||||
}
|
|
||||||
if (await element.directory.exists()) {
|
if (await element.directory.exists()) {
|
||||||
await element.directory.delete(recursive: true);
|
await element.directory.delete(recursive: true);
|
||||||
}
|
}
|
||||||
|
|
@ -456,16 +452,14 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||||
|
|
||||||
ref.read(downloadTasksProvider(syncedItem.id).notifier).update((state) => DownloadStream.empty());
|
ref.read(downloadTasksProvider(syncedItem.id).notifier).update((state) => DownloadStream.empty());
|
||||||
|
|
||||||
final taskId = task?.taskId;
|
ref.read(backgroundDownloaderProvider).cancelTaskWithId(syncedItem.id);
|
||||||
if (taskId != null) {
|
|
||||||
ref.read(backgroundDownloaderProvider).cancelTaskWithId(taskId);
|
|
||||||
}
|
|
||||||
cleanupTemporaryFiles();
|
cleanupTemporaryFiles();
|
||||||
refresh();
|
refresh();
|
||||||
return syncedItem;
|
return syncedItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<DownloadStream?> syncFile(SyncedItem syncItem, bool skipDownload) async {
|
Future<bool?> syncFile(SyncedItem syncItem, bool skipDownload) async {
|
||||||
cleanupTemporaryFiles();
|
cleanupTemporaryFiles();
|
||||||
|
|
||||||
final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
|
final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
|
||||||
|
|
@ -505,8 +499,12 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||||
final downloadUrl = path.joinAll([user.server, "Items", syncItem.id, "Download"]);
|
final downloadUrl = path.joinAll([user.server, "Items", syncItem.id, "Download"]);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!skipDownload && currentTask.task == null) {
|
if (currentTask.task != null) {
|
||||||
|
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(currentTask.id);
|
||||||
|
}
|
||||||
|
if (!skipDownload) {
|
||||||
final downloadTask = DownloadTask(
|
final downloadTask = DownloadTask(
|
||||||
|
taskId: syncItem.id,
|
||||||
url: Uri.parse(downloadUrl).toString(),
|
url: Uri.parse(downloadUrl).toString(),
|
||||||
directory: syncItem.directory.path,
|
directory: syncItem.directory.path,
|
||||||
filename: syncItem.videoFileName,
|
filename: syncItem.videoFileName,
|
||||||
|
|
@ -519,36 +517,9 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||||
allowPause: true,
|
allowPause: true,
|
||||||
);
|
);
|
||||||
|
|
||||||
final defaultDownloadStream =
|
final defaultDownloadStream = DownloadStream(id: syncItem.id, task: downloadTask, status: TaskStatus.enqueued);
|
||||||
DownloadStream(id: syncItem.id, task: downloadTask, progress: 0.0, status: TaskStatus.enqueued);
|
|
||||||
|
|
||||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => defaultDownloadStream);
|
ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => defaultDownloadStream);
|
||||||
|
return await ref.read(backgroundDownloaderProvider).enqueue(downloadTask);
|
||||||
ref.read(backgroundDownloaderProvider).download(
|
|
||||||
downloadTask,
|
|
||||||
onProgress: (progress) {
|
|
||||||
if (progress > 0 && progress < 1) {
|
|
||||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update(
|
|
||||||
(state) => state.copyWith(progress: progress),
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update(
|
|
||||||
(state) => state.copyWith(progress: null),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
onStatus: (status) {
|
|
||||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update(
|
|
||||||
(state) => state.copyWith(status: status),
|
|
||||||
);
|
|
||||||
|
|
||||||
if (status == TaskStatus.complete || status == TaskStatus.canceled) {
|
|
||||||
ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => DownloadStream.empty());
|
|
||||||
}
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
return defaultDownloadStream;
|
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
log(e.toString());
|
log(e.toString());
|
||||||
|
|
|
||||||
|
|
@ -35,11 +35,19 @@ class SyncButton extends ConsumerWidget {
|
||||||
),
|
),
|
||||||
SizedBox.fromSize(
|
SizedBox.fromSize(
|
||||||
size: const Size.fromRadius(10),
|
size: const Size.fromRadius(10),
|
||||||
child: CircularProgressIndicator(
|
child: TweenAnimationBuilder(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: progress,
|
||||||
|
),
|
||||||
|
builder: (context, value, child) => CircularProgressIndicator(
|
||||||
strokeCap: StrokeCap.round,
|
strokeCap: StrokeCap.round,
|
||||||
strokeWidth: 1.5,
|
strokeWidth: 2,
|
||||||
color: status.color(context),
|
color: status.color(context),
|
||||||
value: status == TaskStatus.running ? progress.clamp(0.0, 1.0) : 0,
|
value: status == TaskStatus.running ? value.clamp(0.0, 1.0) : 0,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|
|
||||||
|
|
@ -56,27 +56,43 @@ class SyncProgressBar extends ConsumerWidget {
|
||||||
Widget build(BuildContext context, WidgetRef ref) {
|
Widget build(BuildContext context, WidgetRef ref) {
|
||||||
final downloadStatus = task.status;
|
final downloadStatus = task.status;
|
||||||
final downloadProgress = task.progress;
|
final downloadProgress = task.progress;
|
||||||
|
final downloadSpeed = task.downloadSpeed;
|
||||||
final downloadTask = task.task;
|
final downloadTask = task.task;
|
||||||
|
|
||||||
if (!task.hasDownload) {
|
if (!task.hasDownload) {
|
||||||
return const SizedBox.shrink();
|
return const SizedBox.shrink();
|
||||||
}
|
}
|
||||||
return Column(
|
return Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Text(downloadStatus.name(context)),
|
Text(downloadStatus.name(context)),
|
||||||
|
if (downloadSpeed.isNotEmpty) Opacity(opacity: 0.45, child: Text("($downloadSpeed)")),
|
||||||
|
],
|
||||||
|
),
|
||||||
Row(
|
Row(
|
||||||
crossAxisAlignment: CrossAxisAlignment.center,
|
crossAxisAlignment: CrossAxisAlignment.center,
|
||||||
mainAxisSize: MainAxisSize.min,
|
mainAxisSize: MainAxisSize.min,
|
||||||
spacing: 8,
|
spacing: 8,
|
||||||
children: [
|
children: [
|
||||||
Flexible(
|
Flexible(
|
||||||
child: LinearProgressIndicator(
|
child: TweenAnimationBuilder(
|
||||||
|
duration: const Duration(milliseconds: 250),
|
||||||
|
curve: Curves.easeInOut,
|
||||||
|
tween: Tween<double>(
|
||||||
|
begin: 0,
|
||||||
|
end: downloadProgress,
|
||||||
|
),
|
||||||
|
builder: (context, value, child) => LinearProgressIndicator(
|
||||||
minHeight: 8,
|
minHeight: 8,
|
||||||
value: downloadProgress,
|
value: value,
|
||||||
color: downloadStatus.color(context),
|
color: downloadStatus.color(context),
|
||||||
borderRadius: BorderRadius.circular(8),
|
borderRadius: BorderRadius.circular(8),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")),
|
Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")),
|
||||||
if (downloadTask != null) ...{
|
if (downloadTask != null) ...{
|
||||||
if (downloadStatus != TaskStatus.paused && downloadStatus != TaskStatus.enqueued)
|
if (downloadStatus != TaskStatus.paused && downloadStatus != TaskStatus.enqueued)
|
||||||
|
|
@ -85,14 +101,14 @@ class SyncProgressBar extends ConsumerWidget {
|
||||||
icon: const Icon(IconsaxPlusBold.pause),
|
icon: const Icon(IconsaxPlusBold.pause),
|
||||||
),
|
),
|
||||||
if (downloadStatus == TaskStatus.paused) ...[
|
if (downloadStatus == TaskStatus.paused) ...[
|
||||||
|
IconButton(
|
||||||
|
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
|
||||||
|
icon: const Icon(IconsaxPlusBold.stop),
|
||||||
|
),
|
||||||
IconButton(
|
IconButton(
|
||||||
onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask),
|
onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask),
|
||||||
icon: const Icon(IconsaxPlusBold.play),
|
icon: const Icon(IconsaxPlusBold.play),
|
||||||
),
|
),
|
||||||
IconButton(
|
|
||||||
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
|
|
||||||
icon: const Icon(IconsaxPlusBold.stop),
|
|
||||||
)
|
|
||||||
],
|
],
|
||||||
if (_cancellableStatuses.contains(downloadStatus)) ...[
|
if (_cancellableStatuses.contains(downloadStatus)) ...[
|
||||||
IconButton(
|
IconButton(
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue