From 5a5a4e47030d265dcfeebbdbff9a3de6531824d5 Mon Sep 17 00:00:00 2001 From: PartyDonut Date: Fri, 29 Aug 2025 19:29:47 +0200 Subject: [PATCH] feat: Add download speed to progress bar --- l10n.yaml | 1 - lib/models/syncing/download_stream.dart | 7 ++- lib/models/syncing/sync_item.dart | 8 --- .../sync/background_download_provider.dart | 38 ++++++++++++- lib/providers/sync_provider.dart | 55 +++++-------------- lib/screens/syncing/sync_button.dart | 18 ++++-- lib/screens/syncing/sync_widgets.dart | 36 ++++++++---- 7 files changed, 95 insertions(+), 68 deletions(-) diff --git a/l10n.yaml b/l10n.yaml index 5340fac..b45c580 100644 --- a/l10n.yaml +++ b/l10n.yaml @@ -2,5 +2,4 @@ arb-dir: lib/l10n template-arb-file: app_en.arb output-localization-file: app_localizations.dart nullable-getter: false -synthetic-package: false output-dir: lib/l10n/generated diff --git a/lib/models/syncing/download_stream.dart b/lib/models/syncing/download_stream.dart index 44af29c..5ae1ff1 100644 --- a/lib/models/syncing/download_stream.dart +++ b/lib/models/syncing/download_stream.dart @@ -4,11 +4,13 @@ class DownloadStream { final String id; final dl.DownloadTask? task; final double progress; + final String downloadSpeed; final dl.TaskStatus status; DownloadStream({ required this.id, this.task, - required this.progress, + this.progress = -1, + this.downloadSpeed = "", required this.status, }); @@ -16,6 +18,7 @@ class DownloadStream { : id = '', task = null, progress = -1, + downloadSpeed = "", status = dl.TaskStatus.notFound; bool get hasDownload => progress != -1.0 && status != dl.TaskStatus.notFound && status != dl.TaskStatus.complete; @@ -24,12 +27,14 @@ class DownloadStream { String? id, dl.DownloadTask? task, double? progress, + String? downloadSpeed, dl.TaskStatus? status, }) { return DownloadStream( id: id ?? this.id, task: task ?? this.task, progress: progress ?? this.progress, + downloadSpeed: downloadSpeed ?? this.downloadSpeed, status: status ?? this.status, ); } diff --git a/lib/models/syncing/sync_item.dart b/lib/models/syncing/sync_item.dart index 8cf31d4..7d4a447 100644 --- a/lib/models/syncing/sync_item.dart +++ b/lib/models/syncing/sync_item.dart @@ -82,10 +82,6 @@ abstract class SyncedItem with _$SyncedItem { _ => TaskStatus.notFound, }; - String? get taskId => task?.taskId; - - bool get childHasTask => false; - double get totalProgress => 0.0; bool get hasVideoFile => videoFileName?.isNotEmpty == true && (fileSize ?? 0) > 0; @@ -94,10 +90,6 @@ abstract class SyncedItem with _$SyncedItem { return TaskStatus.notFound; } - double get downloadProgress => 0.0; - TaskStatus get downloadStatus => TaskStatus.notFound; - DownloadTask? get task => null; - Future deleteDatFiles(Ref ref) async { try { await videoFile.delete(); diff --git a/lib/providers/sync/background_download_provider.dart b/lib/providers/sync/background_download_provider.dart index 89cac1f..8ae570c 100644 --- a/lib/providers/sync/background_download_provider.dart +++ b/lib/providers/sync/background_download_provider.dart @@ -1,23 +1,59 @@ +import 'dart:async'; + import 'package:flutter/widgets.dart'; import 'package:background_downloader/background_downloader.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/sync_provider.dart'; import 'package:fladder/util/localization_helper.dart'; part 'background_download_provider.g.dart'; +final itemDownloadGroup = "ITEM_DOWNLOAD_GROUP"; + @Riverpod(keepAlive: true) class BackgroundDownloader extends _$BackgroundDownloader { + late StreamSubscription updateListener; + @override FileDownloader build() { + ref.onDispose( + () => updateListener.cancel(), + ); + final maxDownloads = ref.read(clientSettingsProvider.select((value) => value.maxConcurrentDownloads)); - return FileDownloader() + final downloader = FileDownloader() ..configure( globalConfig: globalConfig(maxDownloads), ) ..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) { diff --git a/lib/providers/sync_provider.dart b/lib/providers/sync_provider.dart index f537e72..f06c354 100644 --- a/lib/providers/sync_provider.dart +++ b/lib/providers/sync_provider.dart @@ -44,7 +44,7 @@ import 'package:fladder/util/localization_helper.dart'; final syncProvider = StateNotifierProvider((ref) => throw UnimplementedError()); -final downloadTasksProvider = StateProvider.family((ref, id) => DownloadStream.empty()); +final downloadTasksProvider = StateProvider.family((ref, id) => DownloadStream.empty()); class SyncNotifier extends StateNotifier { SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) { @@ -315,17 +315,13 @@ class SyncNotifier extends StateNotifier { ) .toList()); - if (item.taskId != null) { - await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!); - } + await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.id); await _db.deleteAllItems([...nestedChildren, item]); for (var i = 0; i < nestedChildren.length; i++) { final element = nestedChildren[i]; - if (element.taskId != null) { - await ref.read(backgroundDownloaderProvider).cancelTaskWithId(element.taskId!); - } + await ref.read(backgroundDownloaderProvider).cancelTaskWithId(element.id); if (await element.directory.exists()) { await element.directory.delete(recursive: true); } @@ -456,16 +452,14 @@ class SyncNotifier extends StateNotifier { ref.read(downloadTasksProvider(syncedItem.id).notifier).update((state) => DownloadStream.empty()); - final taskId = task?.taskId; - if (taskId != null) { - ref.read(backgroundDownloaderProvider).cancelTaskWithId(taskId); - } + ref.read(backgroundDownloaderProvider).cancelTaskWithId(syncedItem.id); + cleanupTemporaryFiles(); refresh(); return syncedItem; } - Future syncFile(SyncedItem syncItem, bool skipDownload) async { + Future syncFile(SyncedItem syncItem, bool skipDownload) async { cleanupTemporaryFiles(); final playbackResponse = await api.itemsItemIdPlaybackInfoPost( @@ -505,8 +499,12 @@ class SyncNotifier extends StateNotifier { final downloadUrl = path.joinAll([user.server, "Items", syncItem.id, "Download"]); try { - if (!skipDownload && currentTask.task == null) { + if (currentTask.task != null) { + await ref.read(backgroundDownloaderProvider).cancelTaskWithId(currentTask.id); + } + if (!skipDownload) { final downloadTask = DownloadTask( + taskId: syncItem.id, url: Uri.parse(downloadUrl).toString(), directory: syncItem.directory.path, filename: syncItem.videoFileName, @@ -519,36 +517,9 @@ class SyncNotifier extends StateNotifier { allowPause: true, ); - final defaultDownloadStream = - DownloadStream(id: syncItem.id, task: downloadTask, progress: 0.0, status: TaskStatus.enqueued); - + final defaultDownloadStream = DownloadStream(id: syncItem.id, task: downloadTask, status: TaskStatus.enqueued); ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => defaultDownloadStream); - - 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; + return await ref.read(backgroundDownloaderProvider).enqueue(downloadTask); } } catch (e) { log(e.toString()); diff --git a/lib/screens/syncing/sync_button.dart b/lib/screens/syncing/sync_button.dart index 8586eeb..cfd95a6 100644 --- a/lib/screens/syncing/sync_button.dart +++ b/lib/screens/syncing/sync_button.dart @@ -35,11 +35,19 @@ class SyncButton extends ConsumerWidget { ), SizedBox.fromSize( size: const Size.fromRadius(10), - child: CircularProgressIndicator( - strokeCap: StrokeCap.round, - strokeWidth: 1.5, - color: status.color(context), - value: status == TaskStatus.running ? progress.clamp(0.0, 1.0) : 0, + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + tween: Tween( + begin: 0, + end: progress, + ), + builder: (context, value, child) => CircularProgressIndicator( + strokeCap: StrokeCap.round, + strokeWidth: 2, + color: status.color(context), + value: status == TaskStatus.running ? value.clamp(0.0, 1.0) : 0, + ), ), ), ], diff --git a/lib/screens/syncing/sync_widgets.dart b/lib/screens/syncing/sync_widgets.dart index ab31052..5e24a51 100644 --- a/lib/screens/syncing/sync_widgets.dart +++ b/lib/screens/syncing/sync_widgets.dart @@ -56,25 +56,41 @@ class SyncProgressBar extends ConsumerWidget { Widget build(BuildContext context, WidgetRef ref) { final downloadStatus = task.status; final downloadProgress = task.progress; + final downloadSpeed = task.downloadSpeed; final downloadTask = task.task; + if (!task.hasDownload) { return const SizedBox.shrink(); } return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Text(downloadStatus.name(context)), + Row( + spacing: 8, + children: [ + Text(downloadStatus.name(context)), + if (downloadSpeed.isNotEmpty) Opacity(opacity: 0.45, child: Text("($downloadSpeed)")), + ], + ), Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, spacing: 8, children: [ Flexible( - child: LinearProgressIndicator( - minHeight: 8, - value: downloadProgress, - color: downloadStatus.color(context), - borderRadius: BorderRadius.circular(8), + child: TweenAnimationBuilder( + duration: const Duration(milliseconds: 250), + curve: Curves.easeInOut, + tween: Tween( + begin: 0, + end: downloadProgress, + ), + builder: (context, value, child) => LinearProgressIndicator( + minHeight: 8, + value: value, + color: downloadStatus.color(context), + borderRadius: BorderRadius.circular(8), + ), ), ), Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")), @@ -85,14 +101,14 @@ class SyncProgressBar extends ConsumerWidget { icon: const Icon(IconsaxPlusBold.pause), ), if (downloadStatus == TaskStatus.paused) ...[ + IconButton( + onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask), + icon: const Icon(IconsaxPlusBold.stop), + ), IconButton( onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask), icon: const Icon(IconsaxPlusBold.play), ), - IconButton( - onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask), - icon: const Icon(IconsaxPlusBold.stop), - ) ], if (_cancellableStatuses.contains(downloadStatus)) ...[ IconButton(