diff --git a/lib/models/syncing/download_stream.dart b/lib/models/syncing/download_stream.dart index 5ae1ff1..7841d35 100644 --- a/lib/models/syncing/download_stream.dart +++ b/lib/models/syncing/download_stream.dart @@ -23,6 +23,8 @@ class DownloadStream { 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({ String? id, dl.DownloadTask? task, diff --git a/lib/providers/sync/background_download_provider.dart b/lib/providers/sync/background_download_provider.dart index 8ae570c..57bcaa4 100644 --- a/lib/providers/sync/background_download_provider.dart +++ b/lib/providers/sync/background_download_provider.dart @@ -44,6 +44,11 @@ class BackgroundDownloader extends _$BackgroundDownloader { if (status == TaskStatus.complete || status == TaskStatus.canceled) { 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(): final progress = update.progress; diff --git a/lib/providers/sync/sync_provider_helpers.dart b/lib/providers/sync/sync_provider_helpers.dart index 74df967..3d4674e 100644 --- a/lib/providers/sync/sync_provider_helpers.dart +++ b/lib/providers/sync/sync_provider_helpers.dart @@ -54,9 +54,10 @@ class SyncDownloadStatus extends _$SyncDownloadStatus { if (childItem.videoFile.existsSync()) { fullySyncedChildren++; } - if (downloadStream.hasDownload) { + if (downloadStream.isEnqueuedOrDownloading) { downloadCount++; - fullProgress += downloadStream.progress; + fullProgress += downloadStream.progress.clamp(0.0, 1.0); + mainStream = mainStream.copyWith( status: mainStream.status != TaskStatus.running ? downloadStream.status : mainStream.status, ); diff --git a/lib/providers/sync_provider.dart b/lib/providers/sync_provider.dart index f484e51..886f32f 100644 --- a/lib/providers/sync_provider.dart +++ b/lib/providers/sync_provider.dart @@ -3,7 +3,6 @@ import 'dart:convert'; import 'dart:developer'; import 'dart:io'; -import 'package:fladder/util/string_extensions.dart'; import 'package:flutter/foundation.dart'; 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/util/duration_extensions.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/string_extensions.dart'; final syncProvider = StateNotifierProvider((ref) => throw UnimplementedError()); final downloadTasksProvider = StateProvider.family((ref, id) => DownloadStream.empty()); +final activeDownloadTasksProvider = StateProvider>((ref) { + return []; +}); + class SyncNotifier extends StateNotifier { SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) { _init(); @@ -130,6 +134,9 @@ class SyncNotifier extends StateNotifier { } Future cleanupTemporaryFiles() async { + final activeDownloads = ref.read(activeDownloadTasksProvider); + if (activeDownloads.isNotEmpty) return; + // List of directories to check final directories = [ //Desktop directory @@ -518,6 +525,11 @@ class SyncNotifier extends StateNotifier { 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); ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => defaultDownloadStream); return await ref.read(backgroundDownloaderProvider).enqueue(downloadTask); @@ -621,7 +633,7 @@ extension SyncNotifierHelpers on SyncNotifier { if (parent == null) { await _db.insertItem(syncItem); } - + return syncItem.copyWith( fileSize: response.mediaSources?.firstOrNull?.size ?? 0, syncing: false, diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 18a3a25..c00c725 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -8,6 +8,7 @@ import 'package:window_manager/window_manager.dart'; import 'package:fladder/models/settings/client_settings_model.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/routes/auto_router.gr.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; @@ -101,6 +102,19 @@ class HomeScreen extends ConsumerWidget { return DestinationModel( label: context.localized.navigationSync, 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), route: const SyncedRoute(), action: () => e.navigate(context), diff --git a/lib/widgets/navigation_scaffold/components/destination_model.dart b/lib/widgets/navigation_scaffold/components/destination_model.dart index f024ea5..f5e4255 100644 --- a/lib/widgets/navigation_scaffold/components/destination_model.dart +++ b/lib/widgets/navigation_scaffold/components/destination_model.dart @@ -12,9 +12,8 @@ class DestinationModel { final PageRouteInfo? route; final Function()? action; final String? tooltip; - final Badge? badge; + final Widget? badge; final AdaptiveFab? floatingActionButton; - // final FloatingActionButton? floatingActionButton; DestinationModel({ required this.label, @@ -25,21 +24,10 @@ class DestinationModel { this.tooltip, this.badge, 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]. NavigationRailDestination toNavigationRailDestination({EdgeInsets? padding}) { - if (badge != null) { - return NavigationRailDestination( - icon: badge!, - label: Text(label), - selectedIcon: badge!, - padding: padding, - ); - } return NavigationRailDestination( icon: icon!, label: Text(label), @@ -50,13 +38,6 @@ class DestinationModel { /// Converts this [DestinationModel] to a [NavigationDrawerDestination] used in a [NavigationDrawer]. NavigationDrawerDestination toNavigationDrawerDestination() { - if (badge != null) { - return NavigationDrawerDestination( - icon: badge!, - label: Text(label), - selectedIcon: badge!, - ); - } return NavigationDrawerDestination( icon: icon!, label: Text(label), @@ -66,13 +47,6 @@ class DestinationModel { /// Converts this [DestinationModel] to a [NavigationDestination] used in a [BottomNavigationBar]. NavigationDestination toNavigationDestination() { - if (badge != null) { - return NavigationDestination( - icon: badge!, - label: label, - selectedIcon: badge!, - ); - } return NavigationDestination( icon: icon!, label: label, @@ -87,6 +61,7 @@ class DestinationModel { label: label, selected: selected, navFocusNode: navFocusNode, + badge: badge, onPressed: action, horizontal: horizontal, expanded: expanded, diff --git a/lib/widgets/navigation_scaffold/components/navigation_button.dart b/lib/widgets/navigation_scaffold/components/navigation_button.dart index 6f993d8..63ed4a7 100644 --- a/lib/widgets/navigation_scaffold/components/navigation_button.dart +++ b/lib/widgets/navigation_scaffold/components/navigation_button.dart @@ -10,6 +10,7 @@ class NavigationButton extends ConsumerStatefulWidget { final String? label; final Widget selectedIcon; final Widget icon; + final Widget? badge; final bool navFocusNode; final bool horizontal; final bool expanded; @@ -23,6 +24,7 @@ class NavigationButton extends ConsumerStatefulWidget { required this.label, required this.selectedIcon, required this.icon, + this.badge, this.navFocusNode = false, this.horizontal = false, this.expanded = false, @@ -95,9 +97,19 @@ class _NavigationButtonState extends ConsumerState { ), ), widget.customIcon ?? - AnimatedSwitcher( - duration: widget.duration, - child: widget.selected ? widget.selectedIcon : widget.icon, + Stack( + alignment: Alignment.center, + 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), if (widget.horizontal && widget.expanded) ...[ @@ -105,10 +117,17 @@ class _NavigationButtonState extends ConsumerState { Expanded( child: ConstrainedBox( constraints: const BoxConstraints(minWidth: 80), - child: Text( - widget.label!, - maxLines: 2, - overflow: TextOverflow.ellipsis, + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + widget.label!, + maxLines: 2, + overflow: TextOverflow.ellipsis, + ), + if (widget.badge != null) widget.badge!, + ], ), ), ), @@ -137,9 +156,19 @@ class _NavigationButtonState extends ConsumerState { spacing: 8, children: [ widget.customIcon ?? - AnimatedSwitcher( - duration: widget.duration, - child: widget.selected ? widget.selectedIcon : widget.icon, + Stack( + alignment: Alignment.center, + 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) Flexible(child: Text(widget.label!)) diff --git a/macos/Podfile.lock b/macos/Podfile.lock index 6503251..92c0755 100644 --- a/macos/Podfile.lock +++ b/macos/Podfile.lock @@ -173,34 +173,34 @@ EXTERNAL SOURCES: :path: Flutter/ephemeral/.symlinks/plugins/window_manager/macos SPEC CHECKSUMS: - audio_service: aa99a6ba2ae7565996015322b0bb024e1d25c6fd - audio_session: eaca2512cf2b39212d724f35d11f46180ad3a33e - connectivity_plus: 4adf20a405e25b42b9c9f87feff8f4b6fde18a4e - desktop_drop: 248706031734554504f939cab1ad4c5fbc9c9c72 - dynamic_color: cb7c2a300ee67ed3bd96c3e852df3af0300bf610 - file_picker: 7584aae6fa07a041af2b36a2655122d42f578c1a + audio_service: cab6c1a0eaf01b5a35b567e11fa67d3cc1956910 + audio_session: 728ae3823d914f809c485d390274861a24b0904e + connectivity_plus: 0a976dfd033b59192912fa3c6c7b54aab5093802 + desktop_drop: e52397f93b3daec9fe1d504f1d5a21b76403d8ae + dynamic_color: 5fdff3953fb3457311091863f72914fc76ea3209 + file_picker: e716a70a9fe5fd9e09ebc922d7541464289443af FlutterMacOS: d0db08ddef1a9af05a5ec4b724367152bb0500b1 - fvp: ecee65308dd86ae46e3c6d5d42cd2aad48095ef5 - just_audio: 4e391f57b79cad2b0674030a00453ca5ce817eed - local_auth_darwin: c3ee6cce0a8d56be34c8ccb66ba31f7f180aaebb + fvp: f4fdb89279e863eb09869bde7ba7fce9e81a16ab + just_audio: a42c63806f16995daf5b219ae1d679deb76e6a79 + local_auth_darwin: 63c73d6d28cc3e239be2b6aa460ea6e317cd5100 mdk: baa616b93f696c7066df0e5ebe057badfa9c462b - media_kit_libs_macos_video: 85a23e549b5f480e72cae3e5634b5514bc692f65 - media_kit_video: fa6564e3799a0a28bff39442334817088b7ca758 - package_info_plus: f0052d280d17aa382b932f399edf32507174e870 - path_provider_foundation: 080d55be775b7414fd5a5ef3ac137b97b097e564 - screen_brightness_macos: 2a3ee243f8051c340381e8e51bcedced8360f421 - screen_retriever_macos: 452e51764a9e1cdb74b3c541238795849f21557f - share_plus: 510bf0af1a42cd602274b4629920c9649c52f4cc - shared_preferences_foundation: 9e1978ff2562383bd5676f64ec4e9aa8fa06a6f7 - sqflite_darwin: 20b2a3a3b70e43edae938624ce550a3cbf66a3d0 + media_kit_libs_macos_video: b3e2bbec2eef97c285f2b1baa7963c67c753fb82 + media_kit_video: c75b07f14d59706c775778e4dd47dd027de8d1e5 + package_info_plus: 12f1c5c2cfe8727ca46cbd0b26677728972d9a5b + path_provider_foundation: 2b6b4c569c0fb62ec74538f866245ac84301af46 + screen_brightness_macos: 2d6d3af2165592d9a55ffcd95b7550970e41ebda + screen_retriever_macos: 776e0fa5d42c6163d2bf772d22478df4b302b161 + share_plus: 1fa619de8392a4398bfaf176d441853922614e89 + shared_preferences_foundation: fcdcbc04712aee1108ac7fda236f363274528f78 + sqflite_darwin: 5a7236e3b501866c1c9befc6771dfd73ffb8702d sqlite3: 73513155ec6979715d3904ef53a8d68892d4032b - sqlite3_flutter_libs: 83f8e9f5b6554077f1d93119fe20ebaa5f3a9ef1 - url_launcher_macos: 0fba8ddabfc33ce0a9afe7c5fef5aab3d8d2d673 - video_player_avfoundation: 2cef49524dd1f16c5300b9cd6efd9611ce03639b - volume_controller: 5c068e6d085c80dadd33fc2c918d2114b775b3dd - wakelock_plus: 917609be14d812ddd9e9528876538b2263aaa03b - webview_flutter_wkwebview: 8ebf4fded22593026f7dbff1fbff31ea98573c8d - window_manager: b729e31d38fb04905235df9ea896128991cad99e + sqlite3_flutter_libs: 86f82662868ee26ff3451f73cac9c5fc2a1f57fa + url_launcher_macos: c82c93949963e55b228a30115bd219499a6fe404 + video_player_avfoundation: 7c6c11d8470e1675df7397027218274b6d2360b3 + volume_controller: 90a5978956cf18ebb7739bf5382fc1b0cfef66d0 + wakelock_plus: 9d63063ffb7af1c215209769067c57103bde719d + webview_flutter_wkwebview: 29eb20d43355b48fe7d07113835b9128f84e3af4 + window_manager: e25faf20d88283a0d46e7b1a759d07261ca27575 PODFILE CHECKSUM: c2e95c8c0fe03c5c57e438583cae4cc732296009