diff --git a/lib/features/downloads/core/download_manager.dart b/lib/features/downloads/core/download_manager.dart index 643301b..77deff8 100644 --- a/lib/features/downloads/core/download_manager.dart +++ b/lib/features/downloads/core/download_manager.dart @@ -92,50 +92,13 @@ class AudiobookDownloadManager { allowPause: allowPause, group: item.id, baseDirectory: downloadDirectory, + updates: Updates.statusAndProgress, // metaData: token ); // _downloadTasks.add(task); tq.add(task); _logger.info('queued task: ${task.taskId}'); } - - FileDownloader().registerCallbacks( - group: item.id, - taskProgressCallback: (update) { - try { - taskProgressCallback?.call(update); - } catch (e) { - _logger.warning('Error in taskProgressCallback: $e'); - } - _logger.info('Group: ${item.id}, Progress Update: ${update.progress}'); - }, - taskStatusCallback: (update) { - try { - taskStatusCallback?.call(update); - } catch (e) { - _logger.warning('Error in taskStatusCallback: $e'); - } - switch (update.status) { - case TaskStatus.complete: - _logger.info('Group: ${item.id}, Download Complete'); - break; - case TaskStatus.failed: - _logger.warning('Group: ${item.id}, Download Failed'); - break; - default: - _logger - .info('Group: ${item.id}, Download Status: ${update.status}'); - } - }, - taskNotificationTapCallback: (task, notificationType) { - try { - taskNotificationTapCallback?.call(task, notificationType); - } catch (e) { - _logger.warning('Error in taskNotificationTapCallback: $e'); - } - _logger.info('Group: ${item.id}, Task: ${task.taskId}'); - }, - ); } String constructFilePath( @@ -152,10 +115,6 @@ class AudiobookDownloadManager { } bool isItemDownloading(String id) { - if (tq.isEmpty) { - return false; - } - return tq.enqueued.any((task) => task.group == id); } @@ -164,6 +123,29 @@ class AudiobookDownloadManager { return fileExists; } + Future> getDownloadedFilesMetadata( + LibraryItemExpanded item, + ) async { + final directory = await getApplicationSupportDirectory(); + final downloadedFiles = []; + for (final file in item.libraryFiles) { + final filePath = constructFilePath(directory, item, file); + if (isFileDownloaded(filePath)) { + downloadedFiles.add(file); + } + } + + return downloadedFiles; + } + + Future getDownloadedSize(LibraryItemExpanded item) async { + final files = await getDownloadedFilesMetadata(item); + return files.fold( + 0, + (previousValue, element) => previousValue + element.metadata.size, + ); + } + Future isItemDownloaded(LibraryItemExpanded item) async { final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { @@ -188,7 +170,7 @@ class AudiobookDownloadManager { } } - Future> getDownloadedFiles(LibraryItemExpanded item) async { + Future> getDownloadedFilesUri(LibraryItemExpanded item) async { final directory = await getApplicationSupportDirectory(); final files = []; for (final file in item.libraryFiles) { @@ -214,12 +196,12 @@ class AudiobookDownloadManager { await fileDownloader.trackTasks(); - _logger.info('Listening to download manager updates'); try { - _updatesSubscription = fileDownloader.updates.listen((event) { - _logger.info('Got event: $event'); + _updatesSubscription = fileDownloader.updates.listen((event) { + _logger.finer('Got event: $event'); _taskStatusController.add(event); }); + _logger.info('Listening to download manager updates'); } catch (e) { _logger.warning('Error when listening to download manager updates: $e'); } diff --git a/lib/features/downloads/providers/download_manager.dart b/lib/features/downloads/providers/download_manager.dart index 88322e1..9fb52dd 100644 --- a/lib/features/downloads/providers/download_manager.dart +++ b/lib/features/downloads/providers/download_manager.dart @@ -3,8 +3,10 @@ import 'package:logging/logging.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:vaani/api/api_provider.dart'; +import 'package:vaani/api/library_item_provider.dart'; import 'package:vaani/features/downloads/core/download_manager.dart' as core; import 'package:vaani/settings/app_settings_provider.dart'; +import 'package:vaani/shared/extensions/item_files.dart'; part 'download_manager.g.dart'; @@ -36,15 +38,11 @@ class SimpleDownloadManager extends _$SimpleDownloadManager { } } -@riverpod +@Riverpod(keepAlive: true) class DownloadManager extends _$DownloadManager { @override core.AudiobookDownloadManager build() { final manager = ref.watch(simpleDownloadManagerProvider); - ref.onDispose(() { - manager.dispose(); - }); - manager.taskUpdateStream.listen((_) { ref.notifyListeners(); }); @@ -87,49 +85,52 @@ class DownloadManager extends _$DownloadManager { class IsItemDownloading extends _$IsItemDownloading { @override bool build(String id) { - final manager = ref.read(downloadManagerProvider); - ref.onDispose(() { - manager.dispose(); - }); - - callback(dynamic _) { - ref.notifyListeners(); - } - - FileDownloader().registerCallbacks( - group: id, - taskStatusCallback: callback, - ); - - ref.onDispose(() { - FileDownloader().unregisterCallbacks(group: id, callback: callback); - }); - + final manager = ref.watch(downloadManagerProvider); return manager.isItemDownloading(id); } } -@Riverpod(keepAlive: true) -class DownloadProgress extends _$DownloadProgress { +@riverpod +class ItemDownloadProgress extends _$ItemDownloadProgress { @override - double build(String id) { - var progress = 0.0; + Future build(String id) async { + final item = await ref.watch(libraryItemProvider(id).future); + final manager = ref.read(downloadManagerProvider); + manager.taskUpdateStream.map((taskUpdate) { + if (taskUpdate is! TaskProgressUpdate) { + return null; + } + if (taskUpdate.task.group == id) { + return taskUpdate; + } + }).listen((task) async { + if (task != null) { + final totalSize = item.totalSize; + // if total size is 0, return 0 + if (totalSize == 0) { + state = const AsyncValue.data(0.0); + return; + } + final downloadedFiles = await manager.getDownloadedFilesMetadata(item); + // calculate total size of downloaded files and total size of item, then divide + // to get percentage + final downloadedSize = downloadedFiles.fold( + 0, + (previousValue, element) => previousValue + element.metadata.size, + ); - void progressCallback(TaskProgressUpdate progress) { - state = progress.progress; - } + final inProgressFileSize = task.progress * task.expectedFileSize; + final totalDownloadedSize = downloadedSize + inProgressFileSize; + final progress = totalDownloadedSize / totalSize; + // if current progress is more than calculated progress, do not update + if (progress < (state.valueOrNull ?? 0.0)) { + return; + } - FileDownloader().registerCallbacks( - group: id, - taskProgressCallback: progressCallback, - ); - - ref.onDispose(() { - FileDownloader() - .unregisterCallbacks(group: id, callback: progressCallback); + state = AsyncValue.data(progress.clamp(0.0, 1.0)); + } }); - - return progress; + return null; } } @@ -141,17 +142,8 @@ FutureOr> downloadHistory( return await FileDownloader().database.allRecords(group: group); } -// @Riverpod(keepAlive: false) -// FutureOr downloadStatus( -// DownloadStatusRef ref, -// LibraryItemExpanded item, -// ) async { -// final manager = ref.read(simpleDownloadManagerProvider); -// return manager.isItemDownloaded(item); -// } - @riverpod -class DownloadStatus extends _$DownloadStatus { +class IsItemDownloaded extends _$IsItemDownloaded { @override FutureOr build( LibraryItemExpanded item, @@ -159,5 +151,4 @@ class DownloadStatus extends _$DownloadStatus { final manager = ref.watch(downloadManagerProvider); return manager.isItemDownloaded(item); } - } diff --git a/lib/features/downloads/providers/download_manager.g.dart b/lib/features/downloads/providers/download_manager.g.dart index 66a2bfe..54eb02c 100644 --- a/lib/features/downloads/providers/download_manager.g.dart +++ b/lib/features/downloads/providers/download_manager.g.dart @@ -174,12 +174,12 @@ final simpleDownloadManagerProvider = NotifierProvider; -String _$downloadManagerHash() => r'bf08bdeda54773a4b6713ad77abb75add3ed30ee'; +String _$downloadManagerHash() => r'9566b772d792b32e1b199d4aa834e28de3b034d0'; /// See also [DownloadManager]. @ProviderFor(DownloadManager) -final downloadManagerProvider = AutoDisposeNotifierProvider.internal( +final downloadManagerProvider = + NotifierProvider.internal( DownloadManager.new, name: r'downloadManagerProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') @@ -189,8 +189,8 @@ final downloadManagerProvider = AutoDisposeNotifierProvider; -String _$isItemDownloadingHash() => r'07e35ae25c096e9c9071667c6303a43f7b5e4cff'; +typedef _$DownloadManager = Notifier; +String _$isItemDownloadingHash() => r'ea43c06393beec828134e08d5f896ddbcfbac8f0'; abstract class _$IsItemDownloading extends BuildlessAutoDisposeNotifier { late final String id; @@ -332,37 +332,39 @@ class _IsItemDownloadingProviderElement String get id => (origin as IsItemDownloadingProvider).id; } -String _$downloadProgressHash() => r'1677f45191181765f6efebdb74206a438a47a4b7'; +String _$itemDownloadProgressHash() => + r'd007c55c6e2e4b992069d0306df8a600225d8598'; -abstract class _$DownloadProgress extends BuildlessNotifier { +abstract class _$ItemDownloadProgress + extends BuildlessAutoDisposeAsyncNotifier { late final String id; - double build( + FutureOr build( String id, ); } -/// See also [DownloadProgress]. -@ProviderFor(DownloadProgress) -const downloadProgressProvider = DownloadProgressFamily(); +/// See also [ItemDownloadProgress]. +@ProviderFor(ItemDownloadProgress) +const itemDownloadProgressProvider = ItemDownloadProgressFamily(); -/// See also [DownloadProgress]. -class DownloadProgressFamily extends Family { - /// See also [DownloadProgress]. - const DownloadProgressFamily(); +/// See also [ItemDownloadProgress]. +class ItemDownloadProgressFamily extends Family> { + /// See also [ItemDownloadProgress]. + const ItemDownloadProgressFamily(); - /// See also [DownloadProgress]. - DownloadProgressProvider call( + /// See also [ItemDownloadProgress]. + ItemDownloadProgressProvider call( String id, ) { - return DownloadProgressProvider( + return ItemDownloadProgressProvider( id, ); } @override - DownloadProgressProvider getProviderOverride( - covariant DownloadProgressProvider provider, + ItemDownloadProgressProvider getProviderOverride( + covariant ItemDownloadProgressProvider provider, ) { return call( provider.id, @@ -381,30 +383,30 @@ class DownloadProgressFamily extends Family { _allTransitiveDependencies; @override - String? get name => r'downloadProgressProvider'; + String? get name => r'itemDownloadProgressProvider'; } -/// See also [DownloadProgress]. -class DownloadProgressProvider - extends NotifierProviderImpl { - /// See also [DownloadProgress]. - DownloadProgressProvider( +/// See also [ItemDownloadProgress]. +class ItemDownloadProgressProvider extends AutoDisposeAsyncNotifierProviderImpl< + ItemDownloadProgress, double?> { + /// See also [ItemDownloadProgress]. + ItemDownloadProgressProvider( String id, ) : this._internal( - () => DownloadProgress()..id = id, - from: downloadProgressProvider, - name: r'downloadProgressProvider', + () => ItemDownloadProgress()..id = id, + from: itemDownloadProgressProvider, + name: r'itemDownloadProgressProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$downloadProgressHash, - dependencies: DownloadProgressFamily._dependencies, + : _$itemDownloadProgressHash, + dependencies: ItemDownloadProgressFamily._dependencies, allTransitiveDependencies: - DownloadProgressFamily._allTransitiveDependencies, + ItemDownloadProgressFamily._allTransitiveDependencies, id: id, ); - DownloadProgressProvider._internal( + ItemDownloadProgressProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -417,8 +419,8 @@ class DownloadProgressProvider final String id; @override - double runNotifierBuild( - covariant DownloadProgress notifier, + FutureOr runNotifierBuild( + covariant ItemDownloadProgress notifier, ) { return notifier.build( id, @@ -426,10 +428,10 @@ class DownloadProgressProvider } @override - Override overrideWith(DownloadProgress Function() create) { + Override overrideWith(ItemDownloadProgress Function() create) { return ProviderOverride( origin: this, - override: DownloadProgressProvider._internal( + override: ItemDownloadProgressProvider._internal( () => create()..id = id, from: from, name: null, @@ -442,13 +444,14 @@ class DownloadProgressProvider } @override - NotifierProviderElement createElement() { - return _DownloadProgressProviderElement(this); + AutoDisposeAsyncNotifierProviderElement + createElement() { + return _ItemDownloadProgressProviderElement(this); } @override bool operator ==(Object other) { - return other is DownloadProgressProvider && other.id == id; + return other is ItemDownloadProgressProvider && other.id == id; } @override @@ -460,23 +463,23 @@ class DownloadProgressProvider } } -mixin DownloadProgressRef on NotifierProviderRef { +mixin ItemDownloadProgressRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `id` of this provider. String get id; } -class _DownloadProgressProviderElement - extends NotifierProviderElement - with DownloadProgressRef { - _DownloadProgressProviderElement(super.provider); +class _ItemDownloadProgressProviderElement + extends AutoDisposeAsyncNotifierProviderElement with ItemDownloadProgressRef { + _ItemDownloadProgressProviderElement(super.provider); @override - String get id => (origin as DownloadProgressProvider).id; + String get id => (origin as ItemDownloadProgressProvider).id; } -String _$downloadStatusHash() => r'3f2bf4e7fb01b9329d31f4797537a307aff70397'; +String _$isItemDownloadedHash() => r'9bb7ba28bdb73e1ba706e849fedc9c7bd67f4b67'; -abstract class _$DownloadStatus +abstract class _$IsItemDownloaded extends BuildlessAutoDisposeAsyncNotifier { late final LibraryItemExpanded item; @@ -485,27 +488,27 @@ abstract class _$DownloadStatus ); } -/// See also [DownloadStatus]. -@ProviderFor(DownloadStatus) -const downloadStatusProvider = DownloadStatusFamily(); +/// See also [IsItemDownloaded]. +@ProviderFor(IsItemDownloaded) +const isItemDownloadedProvider = IsItemDownloadedFamily(); -/// See also [DownloadStatus]. -class DownloadStatusFamily extends Family> { - /// See also [DownloadStatus]. - const DownloadStatusFamily(); +/// See also [IsItemDownloaded]. +class IsItemDownloadedFamily extends Family> { + /// See also [IsItemDownloaded]. + const IsItemDownloadedFamily(); - /// See also [DownloadStatus]. - DownloadStatusProvider call( + /// See also [IsItemDownloaded]. + IsItemDownloadedProvider call( LibraryItemExpanded item, ) { - return DownloadStatusProvider( + return IsItemDownloadedProvider( item, ); } @override - DownloadStatusProvider getProviderOverride( - covariant DownloadStatusProvider provider, + IsItemDownloadedProvider getProviderOverride( + covariant IsItemDownloadedProvider provider, ) { return call( provider.item, @@ -524,30 +527,30 @@ class DownloadStatusFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'downloadStatusProvider'; + String? get name => r'isItemDownloadedProvider'; } -/// See also [DownloadStatus]. -class DownloadStatusProvider - extends AutoDisposeAsyncNotifierProviderImpl { - /// See also [DownloadStatus]. - DownloadStatusProvider( +/// See also [IsItemDownloaded]. +class IsItemDownloadedProvider + extends AutoDisposeAsyncNotifierProviderImpl { + /// See also [IsItemDownloaded]. + IsItemDownloadedProvider( LibraryItemExpanded item, ) : this._internal( - () => DownloadStatus()..item = item, - from: downloadStatusProvider, - name: r'downloadStatusProvider', + () => IsItemDownloaded()..item = item, + from: isItemDownloadedProvider, + name: r'isItemDownloadedProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$downloadStatusHash, - dependencies: DownloadStatusFamily._dependencies, + : _$isItemDownloadedHash, + dependencies: IsItemDownloadedFamily._dependencies, allTransitiveDependencies: - DownloadStatusFamily._allTransitiveDependencies, + IsItemDownloadedFamily._allTransitiveDependencies, item: item, ); - DownloadStatusProvider._internal( + IsItemDownloadedProvider._internal( super._createNotifier, { required super.name, required super.dependencies, @@ -561,7 +564,7 @@ class DownloadStatusProvider @override FutureOr runNotifierBuild( - covariant DownloadStatus notifier, + covariant IsItemDownloaded notifier, ) { return notifier.build( item, @@ -569,10 +572,10 @@ class DownloadStatusProvider } @override - Override overrideWith(DownloadStatus Function() create) { + Override overrideWith(IsItemDownloaded Function() create) { return ProviderOverride( origin: this, - override: DownloadStatusProvider._internal( + override: IsItemDownloadedProvider._internal( () => create()..item = item, from: from, name: null, @@ -585,14 +588,14 @@ class DownloadStatusProvider } @override - AutoDisposeAsyncNotifierProviderElement + AutoDisposeAsyncNotifierProviderElement createElement() { - return _DownloadStatusProviderElement(this); + return _IsItemDownloadedProviderElement(this); } @override bool operator ==(Object other) { - return other is DownloadStatusProvider && other.item == item; + return other is IsItemDownloadedProvider && other.item == item; } @override @@ -604,18 +607,18 @@ class DownloadStatusProvider } } -mixin DownloadStatusRef on AutoDisposeAsyncNotifierProviderRef { +mixin IsItemDownloadedRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `item` of this provider. LibraryItemExpanded get item; } -class _DownloadStatusProviderElement - extends AutoDisposeAsyncNotifierProviderElement - with DownloadStatusRef { - _DownloadStatusProviderElement(super.provider); +class _IsItemDownloadedProviderElement + extends AutoDisposeAsyncNotifierProviderElement + with IsItemDownloadedRef { + _IsItemDownloadedProviderElement(super.provider); @override - LibraryItemExpanded get item => (origin as DownloadStatusProvider).item; + LibraryItemExpanded get item => (origin as IsItemDownloadedProvider).item; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member diff --git a/lib/features/item_viewer/view/library_item_actions.dart b/lib/features/item_viewer/view/library_item_actions.dart index 7e29db5..1e96e14 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -1,7 +1,6 @@ import 'package:background_downloader/background_downloader.dart'; import 'package:flutter/material.dart'; import 'package:flutter_animate/flutter_animate.dart'; -import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:go_router/go_router.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; @@ -11,9 +10,9 @@ import 'package:vaani/features/downloads/providers/download_manager.dart' show downloadHistoryProvider, downloadManagerProvider, - downloadProgressProvider, - downloadStatusProvider, + isItemDownloadedProvider, isItemDownloadingProvider, + itemDownloadProgressProvider, simpleDownloadManagerProvider; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; @@ -214,12 +213,13 @@ class LibItemDownloadButton extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final isItemDownloaded = ref.watch(downloadStatusProvider(item)); + final isItemDownloaded = ref.watch(isItemDownloadedProvider(item)); + if (isItemDownloaded.valueOrNull ?? false) { + return AlreadyItemDownloadedButton(item: item); + } final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id)); - return isItemDownloaded.valueOrNull ?? false - ? AlreadyItemDownloadedButton(item: item) - : isItemDownloading + return isItemDownloading ? ItemCurrentlyInDownloadQueue( item: item, ) @@ -248,9 +248,14 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final progress = ref.watch(downloadProgressProvider(item.id)); - final updatesStream = useStream(ref.watch(downloadManagerProvider).taskUpdateStream); + final progress = + ref.watch(itemDownloadProgressProvider(item.id)).valueOrNull; + if (progress == 1) { + return AlreadyItemDownloadedButton(item: item); + } + + const shimmerDuration = Duration(milliseconds: 1000); return Stack( alignment: Alignment.center, children: [ @@ -260,19 +265,22 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget { ), const Icon( Icons.download, + // color: Theme.of(context).progressIndicatorTheme.color, ) .animate( onPlay: (controller) => controller.repeat(), ) .fade( - begin: 0.5, + duration: shimmerDuration, end: 1, - duration: 1.seconds, + begin: 0.6, + curve: Curves.linearToEaseOut, ) .fade( + duration: shimmerDuration, + end: 0.7, begin: 1, - end: 0.5, - duration: 1.seconds, + curve: Curves.easeInToLinear, ), ], ); @@ -520,7 +528,7 @@ Future libraryItemPlayButtonOnPressed({ final downloadManager = ref.watch(simpleDownloadManagerProvider); final libItem = await ref.read(libraryItemProvider(book.libraryItemId).future); - final downloadedUris = await downloadManager.getDownloadedFiles(libItem); + final downloadedUris = await downloadManager.getDownloadedFilesUri(libItem); setSourceFuture = player.setSourceAudiobook( book, initialPosition: userMediaProgress?.currentTime, diff --git a/lib/shared/extensions/item_files.dart b/lib/shared/extensions/item_files.dart new file mode 100644 index 0000000..b658f24 --- /dev/null +++ b/lib/shared/extensions/item_files.dart @@ -0,0 +1,10 @@ +import 'package:shelfsdk/audiobookshelf_api.dart'; + +extension TotalSize on LibraryItemExpanded { + int get totalSize { + return libraryFiles.fold( + 0, + (previousValue, element) => previousValue + element.metadata.size, + ); + } +}