feat: downloads support (#22)

* feat: enhance download manager with improved logging and task handling

* feat: add total size calculation for library items and improve download manager functionality

* refactor: simplify parameters in queueAudioBookDownload and improve logging message in deleteDownloadedItem
This commit is contained in:
Dr.Blank 2024-09-22 22:05:28 -04:00 committed by GitHub
parent bee1d233bf
commit 792448b0ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 723 additions and 192 deletions

View file

@ -1,5 +1,6 @@
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter/material.dart';
import 'package:flutter_animate/flutter_animate.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 +9,10 @@ import 'package:vaani/constants/hero_tag_conventions.dart';
import 'package:vaani/features/downloads/providers/download_manager.dart'
show
downloadHistoryProvider,
downloadStatusProvider,
downloadManagerProvider,
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';
@ -33,10 +37,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 +94,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 +203,130 @@ 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(isItemDownloadedProvider(item));
if (isItemDownloaded.valueOrNull ?? false) {
return AlreadyItemDownloadedButton(item: item);
}
final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id));
return 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(itemDownloadProgressProvider(item.id)).valueOrNull;
if (progress == 1) {
return AlreadyItemDownloadedButton(item: item);
}
const shimmerDuration = Duration(milliseconds: 1000);
return Stack(
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: progress,
strokeWidth: 2,
),
const Icon(
Icons.download,
// color: Theme.of(context).progressIndicatorTheme.color,
)
.animate(
onPlay: (controller) => controller.repeat(),
)
.fade(
duration: shimmerDuration,
end: 1,
begin: 0.6,
curve: Curves.linearToEaseOut,
)
.fade(
duration: shimmerDuration,
end: 0.7,
begin: 1,
curve: Curves.easeInToLinear,
),
],
);
}
}
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 +337,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 +366,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 +381,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 +400,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}',
),
),
);
}
},
),
],
@ -444,7 +528,7 @@ Future<void> 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,