fix: Downloads resetting with multiple active downloads (#606)

feat: Add sync count icon to menu bars
This commit is contained in:
PartyDonut 2025-11-12 19:51:09 +01:00 committed by GitHub
parent 493f40645c
commit d8f613de07
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 105 additions and 67 deletions

View file

@ -23,6 +23,8 @@ class DownloadStream {
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;
bool get isEnqueuedOrDownloading => status == dl.TaskStatus.enqueued || status == dl.TaskStatus.running;
DownloadStream copyWith({ DownloadStream copyWith({
String? id, String? id,
dl.DownloadTask? task, dl.DownloadTask? task,

View file

@ -44,6 +44,11 @@ class BackgroundDownloader extends _$BackgroundDownloader {
if (status == TaskStatus.complete || status == TaskStatus.canceled) { if (status == TaskStatus.complete || status == TaskStatus.canceled) {
ref.read(downloadTasksProvider(update.task.taskId).notifier).update((state) => DownloadStream.empty()); ref.read(downloadTasksProvider(update.task.taskId).notifier).update((state) => DownloadStream.empty());
ref
.read(activeDownloadTasksProvider.notifier)
.update((state) => state.where((element) => element.taskId != update.task.taskId).toList());
ref.read(syncProvider.notifier).cleanupTemporaryFiles();
} }
case TaskProgressUpdate(): case TaskProgressUpdate():
final progress = update.progress; final progress = update.progress;

View file

@ -54,9 +54,10 @@ class SyncDownloadStatus extends _$SyncDownloadStatus {
if (childItem.videoFile.existsSync()) { if (childItem.videoFile.existsSync()) {
fullySyncedChildren++; fullySyncedChildren++;
} }
if (downloadStream.hasDownload) { if (downloadStream.isEnqueuedOrDownloading) {
downloadCount++; downloadCount++;
fullProgress += downloadStream.progress; fullProgress += downloadStream.progress.clamp(0.0, 1.0);
mainStream = mainStream.copyWith( mainStream = mainStream.copyWith(
status: mainStream.status != TaskStatus.running ? downloadStream.status : mainStream.status, status: mainStream.status != TaskStatus.running ? downloadStream.status : mainStream.status,
); );

View file

@ -3,7 +3,6 @@ import 'dart:convert';
import 'dart:developer'; import 'dart:developer';
import 'dart:io'; import 'dart:io';
import 'package:fladder/util/string_extensions.dart';
import 'package:flutter/foundation.dart'; import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart' hide ConnectionState; import 'package:flutter/material.dart' hide ConnectionState;
@ -42,11 +41,16 @@ import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/duration_extensions.dart'; import 'package:fladder/util/duration_extensions.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/string_extensions.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());
final activeDownloadTasksProvider = StateProvider<List<DownloadTask>>((ref) {
return [];
});
class SyncNotifier extends StateNotifier<SyncSettingsModel> { class SyncNotifier extends StateNotifier<SyncSettingsModel> {
SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) { SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) {
_init(); _init();
@ -130,6 +134,9 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
} }
Future<void> cleanupTemporaryFiles() async { Future<void> cleanupTemporaryFiles() async {
final activeDownloads = ref.read(activeDownloadTasksProvider);
if (activeDownloads.isNotEmpty) return;
// List of directories to check // List of directories to check
final directories = [ final directories = [
//Desktop directory //Desktop directory
@ -518,6 +525,11 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
allowPause: true, allowPause: true,
); );
ref.read(activeDownloadTasksProvider.notifier).update((state) {
final existingTasks = state.where((element) => element.taskId != downloadTask.taskId).toList();
return [...existingTasks, downloadTask];
});
final defaultDownloadStream = DownloadStream(id: syncItem.id, task: downloadTask, 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(downloadTasksProvider(syncItem.id).notifier).update((state) => defaultDownloadStream);
return await ref.read(backgroundDownloaderProvider).enqueue(downloadTask); return await ref.read(backgroundDownloaderProvider).enqueue(downloadTask);
@ -621,7 +633,7 @@ extension SyncNotifierHelpers on SyncNotifier {
if (parent == null) { if (parent == null) {
await _db.insertItem(syncItem); await _db.insertItem(syncItem);
} }
return syncItem.copyWith( return syncItem.copyWith(
fileSize: response.mediaSources?.firstOrNull?.size ?? 0, fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
syncing: false, syncing: false,

View file

@ -8,6 +8,7 @@ import 'package:window_manager/window_manager.dart';
import 'package:fladder/models/settings/client_settings_model.dart'; import 'package:fladder/models/settings/client_settings_model.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/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/routes/auto_router.gr.dart'; import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
@ -101,6 +102,19 @@ class HomeScreen extends ConsumerWidget {
return DestinationModel( return DestinationModel(
label: context.localized.navigationSync, label: context.localized.navigationSync,
icon: Icon(e.icon), icon: Icon(e.icon),
badge: Consumer(
builder: (context, ref, child) {
final length = ref.watch(activeDownloadTasksProvider.select((value) => value.length));
return length != 0
? CircleAvatar(
radius: 10,
child: FittedBox(
child: Text(length.toString()),
),
)
: const SizedBox.shrink();
},
),
selectedIcon: Icon(e.selectedIcon), selectedIcon: Icon(e.selectedIcon),
route: const SyncedRoute(), route: const SyncedRoute(),
action: () => e.navigate(context), action: () => e.navigate(context),

View file

@ -12,9 +12,8 @@ class DestinationModel {
final PageRouteInfo? route; final PageRouteInfo? route;
final Function()? action; final Function()? action;
final String? tooltip; final String? tooltip;
final Badge? badge; final Widget? badge;
final AdaptiveFab? floatingActionButton; final AdaptiveFab? floatingActionButton;
// final FloatingActionButton? floatingActionButton;
DestinationModel({ DestinationModel({
required this.label, required this.label,
@ -25,21 +24,10 @@ class DestinationModel {
this.tooltip, this.tooltip,
this.badge, this.badge,
this.floatingActionButton, this.floatingActionButton,
}) : assert( });
badge == null || icon == null,
'Only one of icon or badge should be provided, not both.',
);
/// Converts this [DestinationModel] to a [NavigationRailDestination] used in a [NavigationRail]. /// Converts this [DestinationModel] to a [NavigationRailDestination] used in a [NavigationRail].
NavigationRailDestination toNavigationRailDestination({EdgeInsets? padding}) { NavigationRailDestination toNavigationRailDestination({EdgeInsets? padding}) {
if (badge != null) {
return NavigationRailDestination(
icon: badge!,
label: Text(label),
selectedIcon: badge!,
padding: padding,
);
}
return NavigationRailDestination( return NavigationRailDestination(
icon: icon!, icon: icon!,
label: Text(label), label: Text(label),
@ -50,13 +38,6 @@ class DestinationModel {
/// Converts this [DestinationModel] to a [NavigationDrawerDestination] used in a [NavigationDrawer]. /// Converts this [DestinationModel] to a [NavigationDrawerDestination] used in a [NavigationDrawer].
NavigationDrawerDestination toNavigationDrawerDestination() { NavigationDrawerDestination toNavigationDrawerDestination() {
if (badge != null) {
return NavigationDrawerDestination(
icon: badge!,
label: Text(label),
selectedIcon: badge!,
);
}
return NavigationDrawerDestination( return NavigationDrawerDestination(
icon: icon!, icon: icon!,
label: Text(label), label: Text(label),
@ -66,13 +47,6 @@ class DestinationModel {
/// Converts this [DestinationModel] to a [NavigationDestination] used in a [BottomNavigationBar]. /// Converts this [DestinationModel] to a [NavigationDestination] used in a [BottomNavigationBar].
NavigationDestination toNavigationDestination() { NavigationDestination toNavigationDestination() {
if (badge != null) {
return NavigationDestination(
icon: badge!,
label: label,
selectedIcon: badge!,
);
}
return NavigationDestination( return NavigationDestination(
icon: icon!, icon: icon!,
label: label, label: label,
@ -87,6 +61,7 @@ class DestinationModel {
label: label, label: label,
selected: selected, selected: selected,
navFocusNode: navFocusNode, navFocusNode: navFocusNode,
badge: badge,
onPressed: action, onPressed: action,
horizontal: horizontal, horizontal: horizontal,
expanded: expanded, expanded: expanded,

View file

@ -10,6 +10,7 @@ class NavigationButton extends ConsumerStatefulWidget {
final String? label; final String? label;
final Widget selectedIcon; final Widget selectedIcon;
final Widget icon; final Widget icon;
final Widget? badge;
final bool navFocusNode; final bool navFocusNode;
final bool horizontal; final bool horizontal;
final bool expanded; final bool expanded;
@ -23,6 +24,7 @@ class NavigationButton extends ConsumerStatefulWidget {
required this.label, required this.label,
required this.selectedIcon, required this.selectedIcon,
required this.icon, required this.icon,
this.badge,
this.navFocusNode = false, this.navFocusNode = false,
this.horizontal = false, this.horizontal = false,
this.expanded = false, this.expanded = false,
@ -95,9 +97,19 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
), ),
), ),
widget.customIcon ?? widget.customIcon ??
AnimatedSwitcher( Stack(
duration: widget.duration, alignment: Alignment.center,
child: widget.selected ? widget.selectedIcon : widget.icon, children: [
AnimatedSwitcher(
duration: widget.duration,
child: widget.selected ? widget.selectedIcon : widget.icon,
),
if (widget.badge != null && !widget.expanded)
Transform.translate(
offset: const Offset(8, -8),
child: widget.badge,
),
],
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
if (widget.horizontal && widget.expanded) ...[ if (widget.horizontal && widget.expanded) ...[
@ -105,10 +117,17 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
Expanded( Expanded(
child: ConstrainedBox( child: ConstrainedBox(
constraints: const BoxConstraints(minWidth: 80), constraints: const BoxConstraints(minWidth: 80),
child: Text( child: Row(
widget.label!, mainAxisAlignment: MainAxisAlignment.spaceBetween,
maxLines: 2, crossAxisAlignment: CrossAxisAlignment.center,
overflow: TextOverflow.ellipsis, children: [
Text(
widget.label!,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (widget.badge != null) widget.badge!,
],
), ),
), ),
), ),
@ -137,9 +156,19 @@ class _NavigationButtonState extends ConsumerState<NavigationButton> {
spacing: 8, spacing: 8,
children: [ children: [
widget.customIcon ?? widget.customIcon ??
AnimatedSwitcher( Stack(
duration: widget.duration, alignment: Alignment.center,
child: widget.selected ? widget.selectedIcon : widget.icon, children: [
AnimatedSwitcher(
duration: widget.duration,
child: widget.selected ? widget.selectedIcon : widget.icon,
),
if (widget.badge != null && !widget.expanded)
Transform.translate(
offset: const Offset(8, -8),
child: widget.badge,
),
],
), ),
if (widget.label != null && widget.horizontal && widget.expanded) if (widget.label != null && widget.horizontal && widget.expanded)
Flexible(child: Text(widget.label!)) Flexible(child: Text(widget.label!))

View file

@ -173,34 +173,34 @@ EXTERNAL SOURCES:
:path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos
SPEC CHECKSUMS: SPEC CHECKSUMS:
audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910
audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e audio_session: 728ae3823d914f809c485d390274861a24b0904e
connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802
desktop_drop: 248706031734554504f939cab1ad4c5fbc9c9c72 desktop_drop: e52397f93b3daec9fe1d504f1d5a21b76403d8ae
dynamic_color: cb7c2a300ee67ed3bd96c3e852df3af0300bf610 dynamic_color: 5fdff3953fb3457311091863f72914fc76ea3209
file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af
FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1
fvp: ecee65308dd86ae46e3c6d5d42cd2aad48095ef5 fvp: f4fdb89279e863eb09869bde7ba7fce9e81a16ab
just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79
local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100
mdk: baa616b93f696c7066df0e5ebe057badfa9c462b mdk: baa616b93f696c7066df0e5ebe057badfa9c462b
media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82
media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5
package_info_plus: f0052d280d17aa382b932f399edf32507174e870 package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b
path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46
screen_brightness_macos: 2a3ee243f8051c340381e8e51bcedced8360f421 screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda
screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161
share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc share_plus: 1fa619de8392a4398bfaf176d441853922614e89
shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78
sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d
sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b
sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 sqlite3_flutter_libs: 86f82662868ee26ff3451f73cac9c5fc2a1f57fa
url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404
video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3
volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd volume_controller: 90a5978956cf18ebb7739bf5382fc1b0cfef66d0
wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d
webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4
window_manager: b729e31d38fb04905235df9ea896128991cad99e window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575
PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009 PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009