feat: enhance download manager with improved logging and task handling

This commit is contained in:
Dr-Blank 2024-09-20 17:53:20 -04:00
parent cac6a96f14
commit 30f87c422b
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
5 changed files with 686 additions and 145 deletions

View file

@ -1,5 +1,6 @@
// download manager to handle download tasks of files // download manager to handle download tasks of files
import 'dart:async';
import 'dart:io'; import 'dart:io';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
@ -38,6 +39,7 @@ class AudiobookDownloadManager {
_logger.fine( _logger.fine(
'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause', 'requiresWiFi: $requiresWiFi, retries: $retries, allowPause: $allowPause',
); );
initDownloadManager();
} }
// the base url for the audio files // the base url for the audio files
@ -55,10 +57,20 @@ class AudiobookDownloadManager {
// whether to allow pausing of downloads // whether to allow pausing of downloads
final bool allowPause; final bool allowPause;
// final List<DownloadTask> _downloadTasks = []; final StreamController<TaskUpdate> _taskStatusController =
StreamController.broadcast();
Future<void> queueAudioBookDownload(LibraryItemExpanded item) async { Stream<TaskUpdate> get taskUpdateStream => _taskStatusController.stream;
_logger.info('queuing download for item: ${item.toJson()}');
late StreamSubscription<TaskUpdate> _updatesSubscription;
Future<void> 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 // create a download task for each file in the item
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
@ -84,15 +96,25 @@ class AudiobookDownloadManager {
); );
// _downloadTasks.add(task); // _downloadTasks.add(task);
tq.add(task); tq.add(task);
_logger.info('queued task: ${task.toJson()}'); _logger.info('queued task: ${task.taskId}');
} }
FileDownloader().registerCallbacks( FileDownloader().registerCallbacks(
group: item.id, group: item.id,
taskProgressCallback: (update) { taskProgressCallback: (update) {
try {
taskProgressCallback?.call(update);
} catch (e) {
_logger.warning('Error in taskProgressCallback: $e');
}
_logger.info('Group: ${item.id}, Progress Update: ${update.progress}'); _logger.info('Group: ${item.id}, Progress Update: ${update.progress}');
}, },
taskStatusCallback: (update) { taskStatusCallback: (update) {
try {
taskStatusCallback?.call(update);
} catch (e) {
_logger.warning('Error in taskStatusCallback: $e');
}
switch (update.status) { switch (update.status) {
case TaskStatus.complete: case TaskStatus.complete:
_logger.info('Group: ${item.id}, Download Complete'); _logger.info('Group: ${item.id}, Download Complete');
@ -106,7 +128,12 @@ class AudiobookDownloadManager {
} }
}, },
taskNotificationTapCallback: (task, notificationType) { 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}'; '${directory.path}/${item.relPath}/${file.metadata.filename}';
// void startDownload() {
// for (final task in _downloadTasks) {
// _logger.fine('enqueuing task: $task');
// FileDownloader().enqueue(task);
// }
// }
void dispose() { void dispose() {
// tq.removeAll(); _updatesSubscription.cancel();
_logger.fine('disposed'); 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) { bool isFileDownloaded(String filePath) {
// Check if the file exists
final fileExists = File(filePath).existsSync(); final fileExists = File(filePath).existsSync();
return fileExists; return fileExists;
} }
@ -145,11 +172,12 @@ class AudiobookDownloadManager {
return false; return false;
} }
} }
_logger.info('all files downloaded for item id: ${item.id}');
return true; return true;
} }
Future<void> deleteDownloadedItem(LibraryItemExpanded item) async { Future<void> deleteDownloadedItem(LibraryItemExpanded item) async {
_logger.info('deleting downloaded item id: ${item.id}');
final directory = await getApplicationSupportDirectory(); final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) { for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file); final filePath = constructFilePath(directory, item, file);
@ -172,14 +200,28 @@ class AudiobookDownloadManager {
return files; return files;
} }
}
Future<void> initDownloadManager() async { Future<void> initDownloadManager() async {
// initialize the download manager // initialize the download manager
var fileDownloader = FileDownloader(); _logger.info('Initializing download manager');
fileDownloader.configureNotification( final fileDownloader = FileDownloader();
running: const TaskNotification('Downloading', 'file: {filename}'),
progressBar: true, _logger.info('Configuring Notification');
); fileDownloader.configureNotification(
await fileDownloader.trackTasks(); 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');
}
}
} }

View file

@ -1,13 +1,15 @@
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:logging/logging.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:shelfsdk/audiobookshelf_api.dart'; import 'package:shelfsdk/audiobookshelf_api.dart';
import 'package:vaani/api/api_provider.dart'; import 'package:vaani/api/api_provider.dart';
import 'package:vaani/features/downloads/core/download_manager.dart' import 'package:vaani/features/downloads/core/download_manager.dart' as core;
as core;
import 'package:vaani/settings/app_settings_provider.dart'; import 'package:vaani/settings/app_settings_provider.dart';
part 'download_manager.g.dart'; part 'download_manager.g.dart';
final _logger = Logger('AudiobookDownloadManagerProvider');
@Riverpod(keepAlive: true) @Riverpod(keepAlive: true)
class SimpleDownloadManager extends _$SimpleDownloadManager { class SimpleDownloadManager extends _$SimpleDownloadManager {
@override @override
@ -25,6 +27,7 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
core.tq.maxConcurrent = downloadSettings.maxConcurrent; core.tq.maxConcurrent = downloadSettings.maxConcurrent;
core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost; core.tq.maxConcurrentByHost = downloadSettings.maxConcurrentByHost;
core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup; core.tq.maxConcurrentByGroup = downloadSettings.maxConcurrentByGroup;
ref.onDispose(() { ref.onDispose(() {
manager.dispose(); 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<void> 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<void> 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 @riverpod
FutureOr<List<TaskRecord>> downloadHistory( FutureOr<List<TaskRecord>> downloadHistory(
DownloadHistoryRef ref, { DownloadHistoryRef ref, {
@ -41,11 +141,23 @@ FutureOr<List<TaskRecord>> downloadHistory(
return await FileDownloader().database.allRecords(group: group); return await FileDownloader().database.allRecords(group: group);
} }
@Riverpod(keepAlive: false) // @Riverpod(keepAlive: false)
FutureOr<bool> downloadStatus( // FutureOr<bool> downloadStatus(
DownloadStatusRef ref, // DownloadStatusRef ref,
LibraryItemExpanded item, // LibraryItemExpanded item,
) async { // ) async {
final manager = ref.read(simpleDownloadManagerProvider); // final manager = ref.read(simpleDownloadManagerProvider);
return manager.isItemDownloaded(item); // return manager.isItemDownloaded(item);
// }
@riverpod
class DownloadStatus extends _$DownloadStatus {
@override
FutureOr<bool> build(
LibraryItemExpanded item,
) {
final manager = ref.watch(downloadManagerProvider);
return manager.isItemDownloaded(item);
}
} }

View file

@ -157,18 +157,344 @@ class _DownloadHistoryProviderElement
String? get group => (origin as DownloadHistoryProvider).group; String? get group => (origin as DownloadHistoryProvider).group;
} }
String _$downloadStatusHash() => r'f37b4678d3c2a7c6e985b0149d72ea0f9b1b42ca'; String _$simpleDownloadManagerHash() =>
r'cec95717c86e422f88f78aa014d29e800e5a2089';
/// See also [downloadStatus]. /// See also [SimpleDownloadManager].
@ProviderFor(downloadStatus) @ProviderFor(SimpleDownloadManager)
final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
core.AudiobookDownloadManager>.internal(
SimpleDownloadManager.new,
name: r'simpleDownloadManagerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$simpleDownloadManagerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
String _$downloadManagerHash() => r'bf08bdeda54773a4b6713ad77abb75add3ed30ee';
/// See also [DownloadManager].
@ProviderFor(DownloadManager)
final downloadManagerProvider = AutoDisposeNotifierProvider<DownloadManager,
core.AudiobookDownloadManager>.internal(
DownloadManager.new,
name: r'downloadManagerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$downloadManagerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DownloadManager = AutoDisposeNotifier<core.AudiobookDownloadManager>;
String _$isItemDownloadingHash() => r'07e35ae25c096e9c9071667c6303a43f7b5e4cff';
abstract class _$IsItemDownloading extends BuildlessAutoDisposeNotifier<bool> {
late final String id;
bool build(
String id,
);
}
/// See also [IsItemDownloading].
@ProviderFor(IsItemDownloading)
const isItemDownloadingProvider = IsItemDownloadingFamily();
/// See also [IsItemDownloading].
class IsItemDownloadingFamily extends Family<bool> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'isItemDownloadingProvider';
}
/// See also [IsItemDownloading].
class IsItemDownloadingProvider
extends AutoDisposeNotifierProviderImpl<IsItemDownloading, bool> {
/// 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<IsItemDownloading, bool> 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<bool> {
/// The parameter `id` of this provider.
String get id;
}
class _IsItemDownloadingProviderElement
extends AutoDisposeNotifierProviderElement<IsItemDownloading, bool>
with IsItemDownloadingRef {
_IsItemDownloadingProviderElement(super.provider);
@override
String get id => (origin as IsItemDownloadingProvider).id;
}
String _$downloadProgressHash() => r'1677f45191181765f6efebdb74206a438a47a4b7';
abstract class _$DownloadProgress extends BuildlessNotifier<double> {
late final String id;
double build(
String id,
);
}
/// See also [DownloadProgress].
@ProviderFor(DownloadProgress)
const downloadProgressProvider = DownloadProgressFamily();
/// See also [DownloadProgress].
class DownloadProgressFamily extends Family<double> {
/// 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<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'downloadProgressProvider';
}
/// See also [DownloadProgress].
class DownloadProgressProvider
extends NotifierProviderImpl<DownloadProgress, double> {
/// 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<DownloadProgress, double> 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<double> {
/// The parameter `id` of this provider.
String get id;
}
class _DownloadProgressProviderElement
extends NotifierProviderElement<DownloadProgress, double>
with DownloadProgressRef {
_DownloadProgressProviderElement(super.provider);
@override
String get id => (origin as DownloadProgressProvider).id;
}
String _$downloadStatusHash() => r'3f2bf4e7fb01b9329d31f4797537a307aff70397';
abstract class _$DownloadStatus
extends BuildlessAutoDisposeAsyncNotifier<bool> {
late final LibraryItemExpanded item;
FutureOr<bool> build(
LibraryItemExpanded item,
);
}
/// See also [DownloadStatus].
@ProviderFor(DownloadStatus)
const downloadStatusProvider = DownloadStatusFamily(); const downloadStatusProvider = DownloadStatusFamily();
/// See also [downloadStatus]. /// See also [DownloadStatus].
class DownloadStatusFamily extends Family<AsyncValue<bool>> { class DownloadStatusFamily extends Family<AsyncValue<bool>> {
/// See also [downloadStatus]. /// See also [DownloadStatus].
const DownloadStatusFamily(); const DownloadStatusFamily();
/// See also [downloadStatus]. /// See also [DownloadStatus].
DownloadStatusProvider call( DownloadStatusProvider call(
LibraryItemExpanded item, LibraryItemExpanded item,
) { ) {
@ -201,16 +527,14 @@ class DownloadStatusFamily extends Family<AsyncValue<bool>> {
String? get name => r'downloadStatusProvider'; String? get name => r'downloadStatusProvider';
} }
/// See also [downloadStatus]. /// See also [DownloadStatus].
class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> { class DownloadStatusProvider
/// See also [downloadStatus]. extends AutoDisposeAsyncNotifierProviderImpl<DownloadStatus, bool> {
/// See also [DownloadStatus].
DownloadStatusProvider( DownloadStatusProvider(
LibraryItemExpanded item, LibraryItemExpanded item,
) : this._internal( ) : this._internal(
(ref) => downloadStatus( () => DownloadStatus()..item = item,
ref as DownloadStatusRef,
item,
),
from: downloadStatusProvider, from: downloadStatusProvider,
name: r'downloadStatusProvider', name: r'downloadStatusProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -236,13 +560,20 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
final LibraryItemExpanded item; final LibraryItemExpanded item;
@override @override
Override overrideWith( FutureOr<bool> runNotifierBuild(
FutureOr<bool> Function(DownloadStatusRef provider) create, covariant DownloadStatus notifier,
) { ) {
return notifier.build(
item,
);
}
@override
Override overrideWith(DownloadStatus Function() create) {
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: DownloadStatusProvider._internal( override: DownloadStatusProvider._internal(
(ref) => create(ref as DownloadStatusRef), () => create()..item = item,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
@ -254,7 +585,8 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
} }
@override @override
AutoDisposeFutureProviderElement<bool> createElement() { AutoDisposeAsyncNotifierProviderElement<DownloadStatus, bool>
createElement() {
return _DownloadStatusProviderElement(this); return _DownloadStatusProviderElement(this);
} }
@ -272,35 +604,18 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
} }
} }
mixin DownloadStatusRef on AutoDisposeFutureProviderRef<bool> { mixin DownloadStatusRef on AutoDisposeAsyncNotifierProviderRef<bool> {
/// The parameter `item` of this provider. /// The parameter `item` of this provider.
LibraryItemExpanded get item; LibraryItemExpanded get item;
} }
class _DownloadStatusProviderElement class _DownloadStatusProviderElement
extends AutoDisposeFutureProviderElement<bool> with DownloadStatusRef { extends AutoDisposeAsyncNotifierProviderElement<DownloadStatus, bool>
with DownloadStatusRef {
_DownloadStatusProviderElement(super.provider); _DownloadStatusProviderElement(super.provider);
@override @override
LibraryItemExpanded get item => (origin as DownloadStatusProvider).item; LibraryItemExpanded get item => (origin as DownloadStatusProvider).item;
} }
String _$simpleDownloadManagerHash() =>
r'cec95717c86e422f88f78aa014d29e800e5a2089';
/// See also [SimpleDownloadManager].
@ProviderFor(SimpleDownloadManager)
final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
core.AudiobookDownloadManager>.internal(
SimpleDownloadManager.new,
name: r'simpleDownloadManagerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$simpleDownloadManagerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member

View file

@ -1,5 +1,7 @@
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.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:go_router/go_router.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart';
import 'package:shelfsdk/audiobookshelf_api.dart' as shelfsdk; 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' import 'package:vaani/features/downloads/providers/download_manager.dart'
show show
downloadHistoryProvider, downloadHistoryProvider,
downloadManagerProvider,
downloadProgressProvider,
downloadStatusProvider, downloadStatusProvider,
isItemDownloadingProvider,
simpleDownloadManagerProvider; simpleDownloadManagerProvider;
import 'package:vaani/features/item_viewer/view/library_item_page.dart'; import 'package:vaani/features/item_viewer/view/library_item_page.dart';
import 'package:vaani/features/per_book_settings/providers/book_settings_provider.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; late final shelfsdk.BookExpanded book;
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final manager = ref.read(simpleDownloadManagerProvider);
final downloadHistory = ref.watch(downloadHistoryProvider(group: item.id)); 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); final apiSettings = ref.watch(apiSettingsProvider);
return Padding( return Padding(
@ -93,64 +95,9 @@ class LibraryItemActions extends HookConsumerWidget {
}, },
icon: const Icon(Icons.share_rounded), icon: const Icon(Icons.share_rounded),
), ),
// check if the book is downloaded using manager.isItemDownloaded // download button
isItemDownloaded.when( LibItemDownloadButton(item: item),
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),
);
},
),
// more button // more button
IconButton( IconButton(
onPressed: () { 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 { class DownloadSheet extends HookConsumerWidget {
const DownloadSheet({ const DownloadSheet({
super.key, super.key,
@ -267,7 +329,7 @@ class DownloadSheet extends HookConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final manager = ref.read(simpleDownloadManagerProvider); final manager = ref.watch(downloadManagerProvider);
return Column( return Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -296,9 +358,9 @@ class DownloadSheet extends HookConsumerWidget {
leading: const Icon( leading: const Icon(
Icons.delete_rounded, Icons.delete_rounded,
), ),
onTap: () { onTap: () async {
// show the delete dialog // show the delete dialog
showDialog( final wasDeleted = await showDialog<bool>(
useRootNavigator: false, useRootNavigator: false,
context: context, context: context,
builder: (context) { builder: (context) {
@ -311,16 +373,18 @@ class DownloadSheet extends HookConsumerWidget {
TextButton( TextButton(
onPressed: () { onPressed: () {
// delete the file // delete the file
manager.deleteDownloadedItem( ref
item, .read(downloadManagerProvider.notifier)
); .deleteDownloadedItem(
GoRouter.of(context).pop(); item,
);
GoRouter.of(context).pop(true);
}, },
child: const Text('Yes'), child: const Text('Yes'),
), ),
TextButton( TextButton(
onPressed: () { onPressed: () {
GoRouter.of(context).pop(); GoRouter.of(context).pop(false);
}, },
child: const Text('No'), 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}',
),
),
);
}
}, },
), ),
], ],

View file

@ -8,7 +8,6 @@ import 'package:just_audio_media_kit/just_audio_media_kit.dart'
import 'package:logging/logging.dart'; import 'package:logging/logging.dart';
import 'package:vaani/api/server_provider.dart'; import 'package:vaani/api/server_provider.dart';
import 'package:vaani/db/storage.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/downloads/providers/download_manager.dart';
import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart'; import 'package:vaani/features/playback_reporting/providers/playback_reporter_provider.dart';
import 'package:vaani/features/player/providers/audiobook_player.dart'; import 'package:vaani/features/player/providers/audiobook_player.dart';
@ -49,9 +48,6 @@ void main() async {
androidNotificationOngoing: true, androidNotificationOngoing: true,
); );
// for initializing the download manager
await initDownloadManager();
// run the app // run the app
runApp( runApp(
const ProviderScope( const ProviderScope(