From 30f87c422b2107c707d3f6a65b6bfb143f245cb1 Mon Sep 17 00:00:00 2001 From: Dr-Blank <64108942+Dr-Blank@users.noreply.github.com> Date: Fri, 20 Sep 2024 17:53:20 -0400 Subject: [PATCH] feat: enhance download manager with improved logging and task handling --- .../downloads/core/download_manager.dart | 94 +++-- .../downloads/providers/download_manager.dart | 130 +++++- .../providers/download_manager.g.dart | 389 ++++++++++++++++-- .../view/library_item_actions.dart | 214 ++++++---- lib/main.dart | 4 - 5 files changed, 686 insertions(+), 145 deletions(-) diff --git a/lib/features/downloads/core/download_manager.dart b/lib/features/downloads/core/download_manager.dart index 99f9f1c..643301b 100644 --- a/lib/features/downloads/core/download_manager.dart +++ b/lib/features/downloads/core/download_manager.dart @@ -1,5 +1,6 @@ // download manager to handle download tasks of files +import 'dart:async'; import 'dart:io'; import 'package:background_downloader/background_downloader.dart'; @@ -38,6 +39,7 @@ class AudiobookDownloadManager { _logger.fine( 'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause', ); + initDownloadManager(); } // the base url for the audio files @@ -55,10 +57,20 @@ class AudiobookDownloadManager { // whether to allow pausing of downloads final bool allowPause; - // final List _downloadTasks = []; + final StreamController _taskStatusController = + StreamController.broadcast(); - Future queueAudioBookDownload(LibraryItemExpanded item) async { - _logger.info('queuing download for item: ${item.toJson()}'); + Stream get taskUpdateStream => _taskStatusController.stream; + + late StreamSubscription _updatesSubscription; + + Future queueAudioBookDownload( + LibraryItemExpanded item, { + void Function(TaskStatusUpdate)? taskStatusCallback, + void Function(TaskProgressUpdate)? taskProgressCallback, + void Function(Task, NotificationType)? taskNotificationTapCallback, + }) async { + _logger.info('queuing download for item: ${item.id}'); // create a download task for each file in the item final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { @@ -84,15 +96,25 @@ class AudiobookDownloadManager { ); // _downloadTasks.add(task); tq.add(task); - _logger.info('queued task: ${task.toJson()}'); + _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'); @@ -106,7 +128,12 @@ class AudiobookDownloadManager { } }, taskNotificationTapCallback: (task, notificationType) { - _logger.info('Group: ${item.id}, Task: ${task.toJson()}'); + try { + taskNotificationTapCallback?.call(task, notificationType); + } catch (e) { + _logger.warning('Error in taskNotificationTapCallback: $e'); + } + _logger.info('Group: ${item.id}, Task: ${task.taskId}'); }, ); } @@ -118,22 +145,22 @@ class AudiobookDownloadManager { ) => '${directory.path}/${item.relPath}/${file.metadata.filename}'; - // void startDownload() { - // for (final task in _downloadTasks) { - // _logger.fine('enqueuing task: $task'); - // FileDownloader().enqueue(task); - // } - // } - void dispose() { - // tq.removeAll(); - _logger.fine('disposed'); + _updatesSubscription.cancel(); + FileDownloader().destroy(); + _logger.fine('Destroyed download manager'); + } + + bool isItemDownloading(String id) { + if (tq.isEmpty) { + return false; + } + + return tq.enqueued.any((task) => task.group == id); } bool isFileDownloaded(String filePath) { - // Check if the file exists final fileExists = File(filePath).existsSync(); - return fileExists; } @@ -145,11 +172,12 @@ class AudiobookDownloadManager { return false; } } - + _logger.info('all files downloaded for item id: ${item.id}'); return true; } Future deleteDownloadedItem(LibraryItemExpanded item) async { + _logger.info('deleting downloaded item id: ${item.id}'); final directory = await getApplicationSupportDirectory(); for (final file in item.libraryFiles) { final filePath = constructFilePath(directory, item, file); @@ -172,14 +200,28 @@ class AudiobookDownloadManager { return files; } -} -Future initDownloadManager() async { - // initialize the download manager - var fileDownloader = FileDownloader(); - fileDownloader.configureNotification( - running: const TaskNotification('Downloading', 'file: {filename}'), - progressBar: true, - ); - await fileDownloader.trackTasks(); + Future initDownloadManager() async { + // initialize the download manager + _logger.info('Initializing download manager'); + final fileDownloader = FileDownloader(); + + _logger.info('Configuring Notification'); + fileDownloader.configureNotification( + running: const TaskNotification('Downloading', 'file: {filename}'), + progressBar: true, + ); + + await fileDownloader.trackTasks(); + + _logger.info('Listening to download manager updates'); + try { + _updatesSubscription = fileDownloader.updates.listen((event) { + _logger.info('Got event: $event'); + _taskStatusController.add(event); + }); + } 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 6a04a8f..88322e1 100644 --- a/lib/features/downloads/providers/download_manager.dart +++ b/lib/features/downloads/providers/download_manager.dart @@ -1,13 +1,15 @@ import 'package:background_downloader/background_downloader.dart'; +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/features/downloads/core/download_manager.dart' - as core; +import 'package:vaani/features/downloads/core/download_manager.dart' as core; import 'package:vaani/settings/app_settings_provider.dart'; part 'download_manager.g.dart'; +final _logger = Logger('AudiobookDownloadManagerProvider'); + @Riverpod(keepAlive: true) class SimpleDownloadManager extends _$SimpleDownloadManager { @override @@ -25,6 +27,7 @@ class SimpleDownloadManager extends _$SimpleDownloadManager { core.tq.maxConcurrent = downloadSettings.maxConcurrent; core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost; core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup; + ref.onDispose(() { manager.dispose(); }); @@ -33,6 +36,103 @@ class SimpleDownloadManager extends _$SimpleDownloadManager { } } +@riverpod +class DownloadManager extends _$DownloadManager { + @override + core.AudiobookDownloadManager build() { + final manager = ref.watch(simpleDownloadManagerProvider); + ref.onDispose(() { + manager.dispose(); + }); + + manager.taskUpdateStream.listen((_) { + ref.notifyListeners(); + }); + return manager; + } + + Future queueAudioBookDownload( + LibraryItemExpanded item, { + void Function(TaskStatusUpdate)? taskStatusCallback, + void Function(TaskProgressUpdate)? taskProgressCallback, + }) async { + await state.queueAudioBookDownload( + item, + taskStatusCallback: (item) { + try { + taskStatusCallback?.call(item); + } catch (e) { + _logger.severe('Error in taskStatusCallback', e); + } + ref.notifyListeners(); + }, + taskProgressCallback: (item) { + try { + taskProgressCallback?.call(item); + } catch (e) { + _logger.severe('Error in taskProgressCallback', e); + } + ref.notifyListeners(); + }, + ); + } + + Future deleteDownloadedItem(LibraryItemExpanded item) async { + await state.deleteDownloadedItem(item); + ref.notifyListeners(); + } +} + +@riverpod +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); + }); + + return manager.isItemDownloading(id); + } +} + +@Riverpod(keepAlive: true) +class DownloadProgress extends _$DownloadProgress { + @override + double build(String id) { + var progress = 0.0; + + void progressCallback(TaskProgressUpdate progress) { + state = progress.progress; + } + + FileDownloader().registerCallbacks( + group: id, + taskProgressCallback: progressCallback, + ); + + ref.onDispose(() { + FileDownloader() + .unregisterCallbacks(group: id, callback: progressCallback); + }); + + return progress; + } +} + @riverpod FutureOr> downloadHistory( DownloadHistoryRef ref, { @@ -41,11 +141,23 @@ 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(keepAlive: false) +// FutureOr downloadStatus( +// DownloadStatusRef ref, +// LibraryItemExpanded item, +// ) async { +// final manager = ref.read(simpleDownloadManagerProvider); +// return manager.isItemDownloaded(item); +// } + +@riverpod +class DownloadStatus extends _$DownloadStatus { + @override + FutureOr build( + LibraryItemExpanded item, + ) { + 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 7a21240..66a2bfe 100644 --- a/lib/features/downloads/providers/download_manager.g.dart +++ b/lib/features/downloads/providers/download_manager.g.dart @@ -157,18 +157,344 @@ class _DownloadHistoryProviderElement String? get group => (origin as DownloadHistoryProvider).group; } -String _$downloadStatusHash() => r'f37b4678d3c2a7c6e985b0149d72ea0f9b1b42ca'; +String _$simpleDownloadManagerHash() => + r'cec95717c86e422f88f78aa014d29e800e5a2089'; -/// See also [downloadStatus]. -@ProviderFor(downloadStatus) +/// See also [SimpleDownloadManager]. +@ProviderFor(SimpleDownloadManager) +final simpleDownloadManagerProvider = NotifierProvider.internal( + SimpleDownloadManager.new, + name: r'simpleDownloadManagerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$simpleDownloadManagerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$SimpleDownloadManager = Notifier; +String _$downloadManagerHash() => r'bf08bdeda54773a4b6713ad77abb75add3ed30ee'; + +/// See also [DownloadManager]. +@ProviderFor(DownloadManager) +final downloadManagerProvider = AutoDisposeNotifierProvider.internal( + DownloadManager.new, + name: r'downloadManagerProvider', + debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') + ? null + : _$downloadManagerHash, + dependencies: null, + allTransitiveDependencies: null, +); + +typedef _$DownloadManager = AutoDisposeNotifier; +String _$isItemDownloadingHash() => r'07e35ae25c096e9c9071667c6303a43f7b5e4cff'; + +abstract class _$IsItemDownloading extends BuildlessAutoDisposeNotifier { + late final String id; + + bool build( + String id, + ); +} + +/// See also [IsItemDownloading]. +@ProviderFor(IsItemDownloading) +const isItemDownloadingProvider = IsItemDownloadingFamily(); + +/// See also [IsItemDownloading]. +class IsItemDownloadingFamily extends Family { + /// See also [IsItemDownloading]. + const IsItemDownloadingFamily(); + + /// See also [IsItemDownloading]. + IsItemDownloadingProvider call( + String id, + ) { + return IsItemDownloadingProvider( + id, + ); + } + + @override + IsItemDownloadingProvider getProviderOverride( + covariant IsItemDownloadingProvider provider, + ) { + return call( + provider.id, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'isItemDownloadingProvider'; +} + +/// See also [IsItemDownloading]. +class IsItemDownloadingProvider + extends AutoDisposeNotifierProviderImpl { + /// See also [IsItemDownloading]. + IsItemDownloadingProvider( + String id, + ) : this._internal( + () => IsItemDownloading()..id = id, + from: isItemDownloadingProvider, + name: r'isItemDownloadingProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$isItemDownloadingHash, + dependencies: IsItemDownloadingFamily._dependencies, + allTransitiveDependencies: + IsItemDownloadingFamily._allTransitiveDependencies, + id: id, + ); + + IsItemDownloadingProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.id, + }) : super.internal(); + + final String id; + + @override + bool runNotifierBuild( + covariant IsItemDownloading notifier, + ) { + return notifier.build( + id, + ); + } + + @override + Override overrideWith(IsItemDownloading Function() create) { + return ProviderOverride( + origin: this, + override: IsItemDownloadingProvider._internal( + () => create()..id = id, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + AutoDisposeNotifierProviderElement createElement() { + return _IsItemDownloadingProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is IsItemDownloadingProvider && other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin IsItemDownloadingRef on AutoDisposeNotifierProviderRef { + /// The parameter `id` of this provider. + String get id; +} + +class _IsItemDownloadingProviderElement + extends AutoDisposeNotifierProviderElement + with IsItemDownloadingRef { + _IsItemDownloadingProviderElement(super.provider); + + @override + String get id => (origin as IsItemDownloadingProvider).id; +} + +String _$downloadProgressHash() => r'1677f45191181765f6efebdb74206a438a47a4b7'; + +abstract class _$DownloadProgress extends BuildlessNotifier { + late final String id; + + double build( + String id, + ); +} + +/// See also [DownloadProgress]. +@ProviderFor(DownloadProgress) +const downloadProgressProvider = DownloadProgressFamily(); + +/// See also [DownloadProgress]. +class DownloadProgressFamily extends Family { + /// See also [DownloadProgress]. + const DownloadProgressFamily(); + + /// See also [DownloadProgress]. + DownloadProgressProvider call( + String id, + ) { + return DownloadProgressProvider( + id, + ); + } + + @override + DownloadProgressProvider getProviderOverride( + covariant DownloadProgressProvider provider, + ) { + return call( + provider.id, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'downloadProgressProvider'; +} + +/// See also [DownloadProgress]. +class DownloadProgressProvider + extends NotifierProviderImpl { + /// See also [DownloadProgress]. + DownloadProgressProvider( + String id, + ) : this._internal( + () => DownloadProgress()..id = id, + from: downloadProgressProvider, + name: r'downloadProgressProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$downloadProgressHash, + dependencies: DownloadProgressFamily._dependencies, + allTransitiveDependencies: + DownloadProgressFamily._allTransitiveDependencies, + id: id, + ); + + DownloadProgressProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.id, + }) : super.internal(); + + final String id; + + @override + double runNotifierBuild( + covariant DownloadProgress notifier, + ) { + return notifier.build( + id, + ); + } + + @override + Override overrideWith(DownloadProgress Function() create) { + return ProviderOverride( + origin: this, + override: DownloadProgressProvider._internal( + () => create()..id = id, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + id: id, + ), + ); + } + + @override + NotifierProviderElement createElement() { + return _DownloadProgressProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is DownloadProgressProvider && other.id == id; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, id.hashCode); + + return _SystemHash.finish(hash); + } +} + +mixin DownloadProgressRef on NotifierProviderRef { + /// The parameter `id` of this provider. + String get id; +} + +class _DownloadProgressProviderElement + extends NotifierProviderElement + with DownloadProgressRef { + _DownloadProgressProviderElement(super.provider); + + @override + String get id => (origin as DownloadProgressProvider).id; +} + +String _$downloadStatusHash() => r'3f2bf4e7fb01b9329d31f4797537a307aff70397'; + +abstract class _$DownloadStatus + extends BuildlessAutoDisposeAsyncNotifier { + late final LibraryItemExpanded item; + + FutureOr build( + LibraryItemExpanded item, + ); +} + +/// See also [DownloadStatus]. +@ProviderFor(DownloadStatus) const downloadStatusProvider = DownloadStatusFamily(); -/// See also [downloadStatus]. +/// See also [DownloadStatus]. class DownloadStatusFamily extends Family> { - /// See also [downloadStatus]. + /// See also [DownloadStatus]. const DownloadStatusFamily(); - /// See also [downloadStatus]. + /// See also [DownloadStatus]. DownloadStatusProvider call( LibraryItemExpanded item, ) { @@ -201,16 +527,14 @@ class DownloadStatusFamily extends Family> { String? get name => r'downloadStatusProvider'; } -/// See also [downloadStatus]. -class DownloadStatusProvider extends AutoDisposeFutureProvider { - /// See also [downloadStatus]. +/// See also [DownloadStatus]. +class DownloadStatusProvider + extends AutoDisposeAsyncNotifierProviderImpl { + /// See also [DownloadStatus]. DownloadStatusProvider( LibraryItemExpanded item, ) : this._internal( - (ref) => downloadStatus( - ref as DownloadStatusRef, - item, - ), + () => DownloadStatus()..item = item, from: downloadStatusProvider, name: r'downloadStatusProvider', debugGetCreateSourceHash: @@ -236,13 +560,20 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider { final LibraryItemExpanded item; @override - Override overrideWith( - FutureOr Function(DownloadStatusRef provider) create, + FutureOr runNotifierBuild( + covariant DownloadStatus notifier, ) { + return notifier.build( + item, + ); + } + + @override + Override overrideWith(DownloadStatus Function() create) { return ProviderOverride( origin: this, override: DownloadStatusProvider._internal( - (ref) => create(ref as DownloadStatusRef), + () => create()..item = item, from: from, name: null, dependencies: null, @@ -254,7 +585,8 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider { } @override - AutoDisposeFutureProviderElement createElement() { + AutoDisposeAsyncNotifierProviderElement + createElement() { return _DownloadStatusProviderElement(this); } @@ -272,35 +604,18 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider { } } -mixin DownloadStatusRef on AutoDisposeFutureProviderRef { +mixin DownloadStatusRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `item` of this provider. LibraryItemExpanded get item; } class _DownloadStatusProviderElement - extends AutoDisposeFutureProviderElement with DownloadStatusRef { + extends AutoDisposeAsyncNotifierProviderElement + with DownloadStatusRef { _DownloadStatusProviderElement(super.provider); @override LibraryItemExpanded get item => (origin as DownloadStatusProvider).item; } - -String _$simpleDownloadManagerHash() => - r'cec95717c86e422f88f78aa014d29e800e5a2089'; - -/// See also [SimpleDownloadManager]. -@ProviderFor(SimpleDownloadManager) -final simpleDownloadManagerProvider = NotifierProvider.internal( - SimpleDownloadManager.new, - name: r'simpleDownloadManagerProvider', - debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') - ? null - : _$simpleDownloadManagerHash, - dependencies: null, - allTransitiveDependencies: null, -); - -typedef _$SimpleDownloadManager = Notifier; // 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 f92d32d..7e29db5 100644 --- a/lib/features/item_viewer/view/library_item_actions.dart +++ b/lib/features/item_viewer/view/library_item_actions.dart @@ -1,5 +1,7 @@ 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; @@ -8,7 +10,10 @@ import 'package:vaani/constants/hero_tag_conventions.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart' show downloadHistoryProvider, + downloadManagerProvider, + downloadProgressProvider, downloadStatusProvider, + isItemDownloadingProvider, simpleDownloadManagerProvider; import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/per_book_settings/providers/book_settings_provider.dart'; @@ -33,10 +38,7 @@ class LibraryItemActions extends HookConsumerWidget { late final shelfsdk.BookExpanded book; @override Widget build(BuildContext context, WidgetRef ref) { - final manager = ref.read(simpleDownloadManagerProvider); final downloadHistory = ref.watch(downloadHistoryProvider(group: item.id)); - final isItemDownloaded = ref.watch(downloadStatusProvider(item)); - final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null; final apiSettings = ref.watch(apiSettingsProvider); return Padding( @@ -93,64 +95,9 @@ class LibraryItemActions extends HookConsumerWidget { }, icon: const Icon(Icons.share_rounded), ), - // check if the book is downloaded using manager.isItemDownloaded - isItemDownloaded.when( - data: (isDownloaded) { - if (isDownloaded) { - // already downloaded button - return IconButton( - onPressed: () { - appLogger - .fine('Pressed already downloaded button'); - // manager.openDownloadedFile(item); - // open popup menu to open or delete the file - showModalBottomSheet( - useRootNavigator: false, - context: context, - builder: (context) { - return Padding( - padding: EdgeInsets.only( - top: 8.0, - bottom: (isBookPlaying - ? playerMinHeight - : 0) + - 8, - ), - child: DownloadSheet( - item: item, - ), - ); - }, - ); - }, - icon: const Icon( - Icons.download_done_rounded, - ), - ); - } - // download button - return IconButton( - onPressed: () { - appLogger.fine('Pressed download button'); - manager.queueAudioBookDownload(item); - }, - icon: const Icon( - Icons.download_rounded, - ), - ); - }, - loading: () => const CircularProgressIndicator(), - error: (error, stackTrace) { - return IconButton( - onPressed: () { - appLogger.warning( - 'Error checking download status: $error', - ); - }, - icon: const Icon(Icons.error_rounded), - ); - }, - ), + // download button + LibItemDownloadButton(item: item), + // more button IconButton( onPressed: () { @@ -257,6 +204,121 @@ class LibraryItemActions extends HookConsumerWidget { } } +class LibItemDownloadButton extends HookConsumerWidget { + const LibItemDownloadButton({ + super.key, + required this.item, + }); + + final shelfsdk.LibraryItemExpanded item; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isItemDownloaded = ref.watch(downloadStatusProvider(item)); + final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id)); + + return isItemDownloaded.valueOrNull ?? false + ? AlreadyItemDownloadedButton(item: item) + : isItemDownloading + ? ItemCurrentlyInDownloadQueue( + item: item, + ) + : IconButton( + onPressed: () { + appLogger.fine('Pressed download button'); + + ref + .read(downloadManagerProvider.notifier) + .queueAudioBookDownload(item); + }, + icon: const Icon( + Icons.download_rounded, + ), + ); + } +} + +class ItemCurrentlyInDownloadQueue extends HookConsumerWidget { + const ItemCurrentlyInDownloadQueue({ + super.key, + required this.item, + }); + + final shelfsdk.LibraryItemExpanded item; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final progress = ref.watch(downloadProgressProvider(item.id)); + final updatesStream = useStream(ref.watch(downloadManagerProvider).taskUpdateStream); + + return Stack( + alignment: Alignment.center, + children: [ + CircularProgressIndicator( + value: progress, + strokeWidth: 2, + ), + const Icon( + Icons.download, + ) + .animate( + onPlay: (controller) => controller.repeat(), + ) + .fade( + begin: 0.5, + end: 1, + duration: 1.seconds, + ) + .fade( + begin: 1, + end: 0.5, + duration: 1.seconds, + ), + ], + ); + } +} + +class AlreadyItemDownloadedButton extends HookConsumerWidget { + const AlreadyItemDownloadedButton({ + super.key, + required this.item, + }); + + final shelfsdk.LibraryItemExpanded item; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final isBookPlaying = ref.watch(audiobookPlayerProvider).book != null; + + return IconButton( + onPressed: () { + appLogger.fine('Pressed already downloaded button'); + // manager.openDownloadedFile(item); + // open popup menu to open or delete the file + showModalBottomSheet( + useRootNavigator: false, + context: context, + builder: (context) { + return Padding( + padding: EdgeInsets.only( + top: 8.0, + bottom: (isBookPlaying ? playerMinHeight : 0) + 8, + ), + child: DownloadSheet( + item: item, + ), + ); + }, + ); + }, + icon: const Icon( + Icons.download_done_rounded, + ), + ); + } +} + class DownloadSheet extends HookConsumerWidget { const DownloadSheet({ super.key, @@ -267,7 +329,7 @@ class DownloadSheet extends HookConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { - final manager = ref.read(simpleDownloadManagerProvider); + final manager = ref.watch(downloadManagerProvider); return Column( mainAxisSize: MainAxisSize.min, @@ -296,9 +358,9 @@ class DownloadSheet extends HookConsumerWidget { leading: const Icon( Icons.delete_rounded, ), - onTap: () { + onTap: () async { // show the delete dialog - showDialog( + final wasDeleted = await showDialog( useRootNavigator: false, context: context, builder: (context) { @@ -311,16 +373,18 @@ class DownloadSheet extends HookConsumerWidget { TextButton( onPressed: () { // delete the file - manager.deleteDownloadedItem( - item, - ); - GoRouter.of(context).pop(); + ref + .read(downloadManagerProvider.notifier) + .deleteDownloadedItem( + item, + ); + GoRouter.of(context).pop(true); }, child: const Text('Yes'), ), TextButton( onPressed: () { - GoRouter.of(context).pop(); + GoRouter.of(context).pop(false); }, child: const Text('No'), ), @@ -328,6 +392,18 @@ class DownloadSheet extends HookConsumerWidget { ); }, ); + + if (wasDeleted ?? false) { + appLogger.fine('Deleted ${item.media.metadata.title}'); + GoRouter.of(context).pop(); + ScaffoldMessenger.of(context).showSnackBar( + SnackBar( + content: Text( + 'Deleted ${item.media.metadata.title}', + ), + ), + ); + } }, ), ], diff --git a/lib/main.dart b/lib/main.dart index 2630802..381d27f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,7 +8,6 @@ import 'package:just_audio_media_kit/just_audio_media_kit.dart' import 'package:logging/logging.dart'; import 'package:vaani/api/server_provider.dart'; import 'package:vaani/db/storage.dart'; -import 'package:vaani/features/downloads/core/download_manager.dart'; import 'package:vaani/features/downloads/providers/download_manager.dart'; import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart'; @@ -49,9 +48,6 @@ void main() async { androidNotificationOngoing: true, ); - // for initializing the download manager - await initDownloadManager(); - // run the app runApp( const ProviderScope(