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 @@
// 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,17 @@ 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,
) 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) {
@ -80,35 +89,13 @@ class AudiobookDownloadManager {
allowPause: allowPause,
group: item.id,
baseDirectory: downloadDirectory,
updates: Updates.statusAndProgress,
// metaData: token
);
// _downloadTasks.add(task);
tq.add(task);
_logger.info('queued task: ${task.toJson()}');
_logger.info('queued task: ${task.taskId}');
}
FileDownloader().registerCallbacks(
group: item.id,
taskProgressCallback: (update) {
_logger.info('Group: ${item.id}, Progress Update: ${update.progress}');
},
taskStatusCallback: (update) {
switch (update.status) {
case TaskStatus.complete:
_logger.info('Group: ${item.id}, Download Complete');
break;
case TaskStatus.failed:
_logger.warning('Group: ${item.id}, Download Failed');
break;
default:
_logger
.info('Group: ${item.id}, Download Status: ${update.status}');
}
},
taskNotificationTapCallback: (task, notificationType) {
_logger.info('Group: ${item.id}, Task: ${task.toJson()}');
},
);
}
String constructFilePath(
@ -118,23 +105,41 @@ 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) {
return tq.enqueued.any((task) => task.group == id);
}
bool isFileDownloaded(String filePath) {
// Check if the file exists
final fileExists = File(filePath).existsSync();
return File(filePath).existsSync();
}
return fileExists;
Future<List<LibraryFile>> getDownloadedFilesMetadata(
LibraryItemExpanded item,
) async {
final directory = await getApplicationSupportDirectory();
final downloadedFiles = <LibraryFile>[];
for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file);
if (isFileDownloaded(filePath)) {
downloadedFiles.add(file);
}
}
return downloadedFiles;
}
Future<int> getDownloadedSize(LibraryItemExpanded item) async {
final files = await getDownloadedFilesMetadata(item);
return files.fold<int>(
0,
(previousValue, element) => previousValue + element.metadata.size,
);
}
Future<bool> isItemDownloaded(LibraryItemExpanded item) async {
@ -145,11 +150,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 with id: ${item.id}');
final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) {
final filePath = constructFilePath(directory, item, file);
@ -160,7 +166,7 @@ class AudiobookDownloadManager {
}
}
Future<List<Uri>> getDownloadedFiles(LibraryItemExpanded item) async {
Future<List<Uri>> getDownloadedFilesUri(LibraryItemExpanded item) async {
final directory = await getApplicationSupportDirectory();
final files = <Uri>[];
for (final file in item.libraryFiles) {
@ -172,14 +178,28 @@ class AudiobookDownloadManager {
return files;
}
}
Future<void> initDownloadManager() async {
Future<void> initDownloadManager() async {
// initialize the download manager
var fileDownloader = FileDownloader();
_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();
try {
_updatesSubscription = fileDownloader.updates.listen((event) {
_logger.finer('Got event: $event');
_taskStatusController.add(event);
});
_logger.info('Listening to download manager updates');
} catch (e) {
_logger.warning('Error when listening to download manager updates: $e');
}
}
}

View file

@ -1,13 +1,17 @@
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/api/library_item_provider.dart';
import 'package:vaani/features/downloads/core/download_manager.dart' as core;
import 'package:vaani/settings/app_settings_provider.dart';
import 'package:vaani/shared/extensions/item_files.dart';
part 'download_manager.g.dart';
final _logger = Logger('AudiobookDownloadManagerProvider');
@Riverpod(keepAlive: true)
class SimpleDownloadManager extends _$SimpleDownloadManager {
@override
@ -25,6 +29,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 +38,102 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
}
}
@Riverpod(keepAlive: true)
class DownloadManager extends _$DownloadManager {
@override
core.AudiobookDownloadManager build() {
final manager = ref.watch(simpleDownloadManagerProvider);
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.watch(downloadManagerProvider);
return manager.isItemDownloading(id);
}
}
@riverpod
class ItemDownloadProgress extends _$ItemDownloadProgress {
@override
Future<double?> build(String id) async {
final item = await ref.watch(libraryItemProvider(id).future);
final manager = ref.read(downloadManagerProvider);
manager.taskUpdateStream.map((taskUpdate) {
if (taskUpdate is! TaskProgressUpdate) {
return null;
}
if (taskUpdate.task.group == id) {
return taskUpdate;
}
}).listen((task) async {
if (task != null) {
final totalSize = item.totalSize;
// if total size is 0, return 0
if (totalSize == 0) {
state = const AsyncValue.data(0.0);
return;
}
final downloadedFiles = await manager.getDownloadedFilesMetadata(item);
// calculate total size of downloaded files and total size of item, then divide
// to get percentage
final downloadedSize = downloadedFiles.fold<int>(
0,
(previousValue, element) => previousValue + element.metadata.size,
);
final inProgressFileSize = task.progress * task.expectedFileSize;
final totalDownloadedSize = downloadedSize + inProgressFileSize;
final progress = totalDownloadedSize / totalSize;
// if current progress is more than calculated progress, do not update
if (progress < (state.valueOrNull ?? 0.0)) {
return;
}
state = AsyncValue.data(progress.clamp(0.0, 1.0));
}
});
return null;
}
}
@riverpod
FutureOr<List<TaskRecord>> downloadHistory(
DownloadHistoryRef ref, {
@ -41,11 +142,13 @@ FutureOr<List<TaskRecord>> downloadHistory(
return await FileDownloader().database.allRecords(group: group);
}
@Riverpod(keepAlive: false)
FutureOr<bool> downloadStatus(
DownloadStatusRef ref,
@riverpod
class IsItemDownloaded extends _$IsItemDownloaded {
@override
FutureOr<bool> build(
LibraryItemExpanded item,
) async {
final manager = ref.read(simpleDownloadManagerProvider);
) {
final manager = ref.watch(downloadManagerProvider);
return manager.isItemDownloaded(item);
}
}

View file

@ -157,29 +157,358 @@ class _DownloadHistoryProviderElement
String? get group => (origin as DownloadHistoryProvider).group;
}
String _$downloadStatusHash() => r'f37b4678d3c2a7c6e985b0149d72ea0f9b1b42ca';
String _$simpleDownloadManagerHash() =>
r'cec95717c86e422f88f78aa014d29e800e5a2089';
/// See also [downloadStatus].
@ProviderFor(downloadStatus)
const downloadStatusProvider = DownloadStatusFamily();
/// 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,
);
/// See also [downloadStatus].
class DownloadStatusFamily extends Family<AsyncValue<bool>> {
/// See also [downloadStatus].
const DownloadStatusFamily();
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
String _$downloadManagerHash() => r'9566b772d792b32e1b199d4aa834e28de3b034d0';
/// See also [downloadStatus].
DownloadStatusProvider call(
/// See also [DownloadManager].
@ProviderFor(DownloadManager)
final downloadManagerProvider =
NotifierProvider<DownloadManager, core.AudiobookDownloadManager>.internal(
DownloadManager.new,
name: r'downloadManagerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$downloadManagerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$DownloadManager = Notifier<core.AudiobookDownloadManager>;
String _$isItemDownloadingHash() => r'ea43c06393beec828134e08d5f896ddbcfbac8f0';
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 _$itemDownloadProgressHash() =>
r'd007c55c6e2e4b992069d0306df8a600225d8598';
abstract class _$ItemDownloadProgress
extends BuildlessAutoDisposeAsyncNotifier<double?> {
late final String id;
FutureOr<double?> build(
String id,
);
}
/// See also [ItemDownloadProgress].
@ProviderFor(ItemDownloadProgress)
const itemDownloadProgressProvider = ItemDownloadProgressFamily();
/// See also [ItemDownloadProgress].
class ItemDownloadProgressFamily extends Family<AsyncValue<double?>> {
/// See also [ItemDownloadProgress].
const ItemDownloadProgressFamily();
/// See also [ItemDownloadProgress].
ItemDownloadProgressProvider call(
String id,
) {
return ItemDownloadProgressProvider(
id,
);
}
@override
ItemDownloadProgressProvider getProviderOverride(
covariant ItemDownloadProgressProvider 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'itemDownloadProgressProvider';
}
/// See also [ItemDownloadProgress].
class ItemDownloadProgressProvider extends AutoDisposeAsyncNotifierProviderImpl<
ItemDownloadProgress, double?> {
/// See also [ItemDownloadProgress].
ItemDownloadProgressProvider(
String id,
) : this._internal(
() => ItemDownloadProgress()..id = id,
from: itemDownloadProgressProvider,
name: r'itemDownloadProgressProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$itemDownloadProgressHash,
dependencies: ItemDownloadProgressFamily._dependencies,
allTransitiveDependencies:
ItemDownloadProgressFamily._allTransitiveDependencies,
id: id,
);
ItemDownloadProgressProvider._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
FutureOr<double?> runNotifierBuild(
covariant ItemDownloadProgress notifier,
) {
return notifier.build(
id,
);
}
@override
Override overrideWith(ItemDownloadProgress Function() create) {
return ProviderOverride(
origin: this,
override: ItemDownloadProgressProvider._internal(
() => create()..id = id,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
id: id,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<ItemDownloadProgress, double?>
createElement() {
return _ItemDownloadProgressProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is ItemDownloadProgressProvider && 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 ItemDownloadProgressRef on AutoDisposeAsyncNotifierProviderRef<double?> {
/// The parameter `id` of this provider.
String get id;
}
class _ItemDownloadProgressProviderElement
extends AutoDisposeAsyncNotifierProviderElement<ItemDownloadProgress,
double?> with ItemDownloadProgressRef {
_ItemDownloadProgressProviderElement(super.provider);
@override
String get id => (origin as ItemDownloadProgressProvider).id;
}
String _$isItemDownloadedHash() => r'9bb7ba28bdb73e1ba706e849fedc9c7bd67f4b67';
abstract class _$IsItemDownloaded
extends BuildlessAutoDisposeAsyncNotifier<bool> {
late final LibraryItemExpanded item;
FutureOr<bool> build(
LibraryItemExpanded item,
);
}
/// See also [IsItemDownloaded].
@ProviderFor(IsItemDownloaded)
const isItemDownloadedProvider = IsItemDownloadedFamily();
/// See also [IsItemDownloaded].
class IsItemDownloadedFamily extends Family<AsyncValue<bool>> {
/// See also [IsItemDownloaded].
const IsItemDownloadedFamily();
/// See also [IsItemDownloaded].
IsItemDownloadedProvider call(
LibraryItemExpanded item,
) {
return DownloadStatusProvider(
return IsItemDownloadedProvider(
item,
);
}
@override
DownloadStatusProvider getProviderOverride(
covariant DownloadStatusProvider provider,
IsItemDownloadedProvider getProviderOverride(
covariant IsItemDownloadedProvider provider,
) {
return call(
provider.item,
@ -198,32 +527,30 @@ class DownloadStatusFamily extends Family<AsyncValue<bool>> {
_allTransitiveDependencies;
@override
String? get name => r'downloadStatusProvider';
String? get name => r'isItemDownloadedProvider';
}
/// See also [downloadStatus].
class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
/// See also [downloadStatus].
DownloadStatusProvider(
/// See also [IsItemDownloaded].
class IsItemDownloadedProvider
extends AutoDisposeAsyncNotifierProviderImpl<IsItemDownloaded, bool> {
/// See also [IsItemDownloaded].
IsItemDownloadedProvider(
LibraryItemExpanded item,
) : this._internal(
(ref) => downloadStatus(
ref as DownloadStatusRef,
item,
),
from: downloadStatusProvider,
name: r'downloadStatusProvider',
() => IsItemDownloaded()..item = item,
from: isItemDownloadedProvider,
name: r'isItemDownloadedProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$downloadStatusHash,
dependencies: DownloadStatusFamily._dependencies,
: _$isItemDownloadedHash,
dependencies: IsItemDownloadedFamily._dependencies,
allTransitiveDependencies:
DownloadStatusFamily._allTransitiveDependencies,
IsItemDownloadedFamily._allTransitiveDependencies,
item: item,
);
DownloadStatusProvider._internal(
IsItemDownloadedProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
@ -236,13 +563,20 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
final LibraryItemExpanded item;
@override
Override overrideWith(
FutureOr<bool> Function(DownloadStatusRef provider) create,
FutureOr<bool> runNotifierBuild(
covariant IsItemDownloaded notifier,
) {
return notifier.build(
item,
);
}
@override
Override overrideWith(IsItemDownloaded Function() create) {
return ProviderOverride(
origin: this,
override: DownloadStatusProvider._internal(
(ref) => create(ref as DownloadStatusRef),
override: IsItemDownloadedProvider._internal(
() => create()..item = item,
from: from,
name: null,
dependencies: null,
@ -254,13 +588,14 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
}
@override
AutoDisposeFutureProviderElement<bool> createElement() {
return _DownloadStatusProviderElement(this);
AutoDisposeAsyncNotifierProviderElement<IsItemDownloaded, bool>
createElement() {
return _IsItemDownloadedProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DownloadStatusProvider && other.item == item;
return other is IsItemDownloadedProvider && other.item == item;
}
@override
@ -272,35 +607,18 @@ class DownloadStatusProvider extends AutoDisposeFutureProvider<bool> {
}
}
mixin DownloadStatusRef on AutoDisposeFutureProviderRef<bool> {
mixin IsItemDownloadedRef on AutoDisposeAsyncNotifierProviderRef<bool> {
/// The parameter `item` of this provider.
LibraryItemExpanded get item;
}
class _DownloadStatusProviderElement
extends AutoDisposeFutureProviderElement<bool> with DownloadStatusRef {
_DownloadStatusProviderElement(super.provider);
class _IsItemDownloadedProviderElement
extends AutoDisposeAsyncNotifierProviderElement<IsItemDownloaded, bool>
with IsItemDownloadedRef {
_IsItemDownloadedProviderElement(super.provider);
@override
LibraryItemExpanded get item => (origin as DownloadStatusProvider).item;
LibraryItemExpanded get item => (origin as IsItemDownloadedProvider).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,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),
);
},
),
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(
ref
.read(downloadManagerProvider.notifier)
.deleteDownloadedItem(
item,
);
GoRouter.of(context).pop();
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,

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(

View file

@ -0,0 +1,10 @@
import 'package:shelfsdk/audiobookshelf_api.dart';
extension TotalSize on LibraryItemExpanded {
int get totalSize {
return libraryFiles.fold(
0,
(previousValue, element) => previousValue + element.metadata.size,
);
}
}