feat: Add download speed to progress bar

This commit is contained in:
PartyDonut 2025-08-29 19:29:47 +02:00
parent d22d340181
commit 5a5a4e4703
7 changed files with 95 additions and 68 deletions

View file

@ -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

View file

@ -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,
); );
} }

View file

@ -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();

View file

@ -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) {

View file

@ -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());

View file

@ -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,
),
), ),
), ),
], ],

View file

@ -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(