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
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<DownloadTask> _downloadTasks = [];
final StreamController<TaskUpdate> _taskStatusController =
StreamController.broadcast();
Future<void> queueAudioBookDownload(LibraryItemExpanded item) async {
_logger.info('queuing download for item: ${item.toJson()}');
Stream<TaskUpdate> get taskUpdateStream => _taskStatusController.stream;
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
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<void> 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<void> initDownloadManager() async {
// initialize the download manager
var fileDownloader = FileDownloader();
fileDownloader.configureNotification(
running: const TaskNotification('Downloading', 'file: {filename}'),
progressBar: true,
);
await fileDownloader.trackTasks();
Future<void> 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');
}
}
}

View file

@ -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<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
FutureOr<List<TaskRecord>> downloadHistory(
DownloadHistoryRef ref, {
@ -41,11 +141,23 @@ FutureOr<List<TaskRecord>> downloadHistory(
return await FileDownloader().database.allRecords(group: group);
}
@Riverpod(keepAlive: false)
FutureOr<bool> downloadStatus(
DownloadStatusRef ref,
LibraryItemExpanded item,
) async {
final manager = ref.read(simpleDownloadManagerProvider);
return manager.isItemDownloaded(item);
// @Riverpod(keepAlive: false)
// FutureOr<bool> downloadStatus(
// DownloadStatusRef ref,
// LibraryItemExpanded item,
// ) async {
// final manager = ref.read(simpleDownloadManagerProvider);
// 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 _$downloadStatusHash() => r'f37b4678d3c2a7c6e985b0149d72ea0f9b1b42ca';
String _$simpleDownloadManagerHash() =>
r'cec95717c86e422f88f78aa014d29e800e5a2089';
/// See also [downloadStatus].
@ProviderFor(downloadStatus)
/// 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>;
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();
/// See also [downloadStatus].
/// See also [DownloadStatus].
class DownloadStatusFamily extends Family<AsyncValue<bool>> {
/// 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<AsyncValue<bool>> {
String? get name => r'downloadStatusProvider';
}
/// See also [downloadStatus].
class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
/// See also [downloadStatus].
/// See also [DownloadStatus].
class DownloadStatusProvider
extends AutoDisposeAsyncNotifierProviderImpl<DownloadStatus, bool> {
/// 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<bool> {
final LibraryItemExpanded item;
@override
Override overrideWith(
FutureOr<bool> Function(DownloadStatusRef provider) create,
FutureOr<bool> 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<bool> {
}
@override
AutoDisposeFutureProviderElement<bool> createElement() {
AutoDisposeAsyncNotifierProviderElement<DownloadStatus, bool>
createElement() {
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.
LibraryItemExpanded get item;
}
class _DownloadStatusProviderElement
extends AutoDisposeFutureProviderElement<bool> with DownloadStatusRef {
extends AutoDisposeAsyncNotifierProviderElement<DownloadStatus, bool>
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<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: 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: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<bool>(
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}',
),
),
);
}
},
),
],

View file

@ -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(