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

This commit is contained in:
Dr-Blank 2024-09-22 21:51:52 -04:00
parent 30f87c422b
commit b0cc1ff8cb
No known key found for this signature in database
GPG key ID: 7452CC63F210A266
5 changed files with 190 additions and 196 deletions

View file

@ -92,50 +92,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.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');
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) {
try {
taskNotificationTapCallback?.call(task, notificationType);
} catch (e) {
_logger.warning('Error in taskNotificationTapCallback: $e');
}
_logger.info('Group: ${item.id}, Task: ${task.taskId}');
},
);
}
String constructFilePath(
@ -152,10 +115,6 @@ class AudiobookDownloadManager {
}
bool isItemDownloading(String id) {
if (tq.isEmpty) {
return false;
}
return tq.enqueued.any((task) => task.group == id);
}
@ -164,6 +123,29 @@ class AudiobookDownloadManager {
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 {
final directory = await getApplicationSupportDirectory();
for (final file in item.libraryFiles) {
@ -188,7 +170,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) {
@ -214,12 +196,12 @@ class AudiobookDownloadManager {
await fileDownloader.trackTasks();
_logger.info('Listening to download manager updates');
try {
_updatesSubscription = fileDownloader.updates.listen((event) {
_logger.info('Got event: $event');
_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

@ -3,8 +3,10 @@ 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/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';
@ -36,15 +38,11 @@ class SimpleDownloadManager extends _$SimpleDownloadManager {
}
}
@riverpod
@Riverpod(keepAlive: true)
class DownloadManager extends _$DownloadManager {
@override
core.AudiobookDownloadManager build() {
final manager = ref.watch(simpleDownloadManagerProvider);
ref.onDispose(() {
manager.dispose();
});
manager.taskUpdateStream.listen((_) {
ref.notifyListeners();
});
@ -87,49 +85,52 @@ class DownloadManager extends _$DownloadManager {
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);
});
final manager = ref.watch(downloadManagerProvider);
return manager.isItemDownloading(id);
}
}
@Riverpod(keepAlive: true)
class DownloadProgress extends _$DownloadProgress {
@riverpod
class ItemDownloadProgress extends _$ItemDownloadProgress {
@override
double build(String id) {
var progress = 0.0;
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,
);
void progressCallback(TaskProgressUpdate progress) {
state = progress.progress;
}
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;
}
FileDownloader().registerCallbacks(
group: id,
taskProgressCallback: progressCallback,
);
ref.onDispose(() {
FileDownloader()
.unregisterCallbacks(group: id, callback: progressCallback);
state = AsyncValue.data(progress.clamp(0.0, 1.0));
}
});
return progress;
return null;
}
}
@ -141,17 +142,8 @@ 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
class DownloadStatus extends _$DownloadStatus {
class IsItemDownloaded extends _$IsItemDownloaded {
@override
FutureOr<bool> build(
LibraryItemExpanded item,
@ -159,5 +151,4 @@ class DownloadStatus extends _$DownloadStatus {
final manager = ref.watch(downloadManagerProvider);
return manager.isItemDownloaded(item);
}
}

View file

@ -174,12 +174,12 @@ final simpleDownloadManagerProvider = NotifierProvider<SimpleDownloadManager,
);
typedef _$SimpleDownloadManager = Notifier<core.AudiobookDownloadManager>;
String _$downloadManagerHash() => r'bf08bdeda54773a4b6713ad77abb75add3ed30ee';
String _$downloadManagerHash() => r'9566b772d792b32e1b199d4aa834e28de3b034d0';
/// See also [DownloadManager].
@ProviderFor(DownloadManager)
final downloadManagerProvider = AutoDisposeNotifierProvider<DownloadManager,
core.AudiobookDownloadManager>.internal(
final downloadManagerProvider =
NotifierProvider<DownloadManager, core.AudiobookDownloadManager>.internal(
DownloadManager.new,
name: r'downloadManagerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
@ -189,8 +189,8 @@ final downloadManagerProvider = AutoDisposeNotifierProvider<DownloadManager,
allTransitiveDependencies: null,
);
typedef _$DownloadManager = AutoDisposeNotifier<core.AudiobookDownloadManager>;
String _$isItemDownloadingHash() => r'07e35ae25c096e9c9071667c6303a43f7b5e4cff';
typedef _$DownloadManager = Notifier<core.AudiobookDownloadManager>;
String _$isItemDownloadingHash() => r'ea43c06393beec828134e08d5f896ddbcfbac8f0';
abstract class _$IsItemDownloading extends BuildlessAutoDisposeNotifier<bool> {
late final String id;
@ -332,37 +332,39 @@ class _IsItemDownloadingProviderElement
String get id => (origin as IsItemDownloadingProvider).id;
}
String _$downloadProgressHash() => r'1677f45191181765f6efebdb74206a438a47a4b7';
String _$itemDownloadProgressHash() =>
r'd007c55c6e2e4b992069d0306df8a600225d8598';
abstract class _$DownloadProgress extends BuildlessNotifier<double> {
abstract class _$ItemDownloadProgress
extends BuildlessAutoDisposeAsyncNotifier<double?> {
late final String id;
double build(
FutureOr<double?> build(
String id,
);
}
/// See also [DownloadProgress].
@ProviderFor(DownloadProgress)
const downloadProgressProvider = DownloadProgressFamily();
/// See also [ItemDownloadProgress].
@ProviderFor(ItemDownloadProgress)
const itemDownloadProgressProvider = ItemDownloadProgressFamily();
/// See also [DownloadProgress].
class DownloadProgressFamily extends Family<double> {
/// See also [DownloadProgress].
const DownloadProgressFamily();
/// See also [ItemDownloadProgress].
class ItemDownloadProgressFamily extends Family<AsyncValue<double?>> {
/// See also [ItemDownloadProgress].
const ItemDownloadProgressFamily();
/// See also [DownloadProgress].
DownloadProgressProvider call(
/// See also [ItemDownloadProgress].
ItemDownloadProgressProvider call(
String id,
) {
return DownloadProgressProvider(
return ItemDownloadProgressProvider(
id,
);
}
@override
DownloadProgressProvider getProviderOverride(
covariant DownloadProgressProvider provider,
ItemDownloadProgressProvider getProviderOverride(
covariant ItemDownloadProgressProvider provider,
) {
return call(
provider.id,
@ -381,30 +383,30 @@ class DownloadProgressFamily extends Family<double> {
_allTransitiveDependencies;
@override
String? get name => r'downloadProgressProvider';
String? get name => r'itemDownloadProgressProvider';
}
/// See also [DownloadProgress].
class DownloadProgressProvider
extends NotifierProviderImpl<DownloadProgress, double> {
/// See also [DownloadProgress].
DownloadProgressProvider(
/// See also [ItemDownloadProgress].
class ItemDownloadProgressProvider extends AutoDisposeAsyncNotifierProviderImpl<
ItemDownloadProgress, double?> {
/// See also [ItemDownloadProgress].
ItemDownloadProgressProvider(
String id,
) : this._internal(
() => DownloadProgress()..id = id,
from: downloadProgressProvider,
name: r'downloadProgressProvider',
() => ItemDownloadProgress()..id = id,
from: itemDownloadProgressProvider,
name: r'itemDownloadProgressProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$downloadProgressHash,
dependencies: DownloadProgressFamily._dependencies,
: _$itemDownloadProgressHash,
dependencies: ItemDownloadProgressFamily._dependencies,
allTransitiveDependencies:
DownloadProgressFamily._allTransitiveDependencies,
ItemDownloadProgressFamily._allTransitiveDependencies,
id: id,
);
DownloadProgressProvider._internal(
ItemDownloadProgressProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
@ -417,8 +419,8 @@ class DownloadProgressProvider
final String id;
@override
double runNotifierBuild(
covariant DownloadProgress notifier,
FutureOr<double?> runNotifierBuild(
covariant ItemDownloadProgress notifier,
) {
return notifier.build(
id,
@ -426,10 +428,10 @@ class DownloadProgressProvider
}
@override
Override overrideWith(DownloadProgress Function() create) {
Override overrideWith(ItemDownloadProgress Function() create) {
return ProviderOverride(
origin: this,
override: DownloadProgressProvider._internal(
override: ItemDownloadProgressProvider._internal(
() => create()..id = id,
from: from,
name: null,
@ -442,13 +444,14 @@ class DownloadProgressProvider
}
@override
NotifierProviderElement<DownloadProgress, double> createElement() {
return _DownloadProgressProviderElement(this);
AutoDisposeAsyncNotifierProviderElement<ItemDownloadProgress, double?>
createElement() {
return _ItemDownloadProgressProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DownloadProgressProvider && other.id == id;
return other is ItemDownloadProgressProvider && other.id == id;
}
@override
@ -460,23 +463,23 @@ class DownloadProgressProvider
}
}
mixin DownloadProgressRef on NotifierProviderRef<double> {
mixin ItemDownloadProgressRef on AutoDisposeAsyncNotifierProviderRef<double?> {
/// The parameter `id` of this provider.
String get id;
}
class _DownloadProgressProviderElement
extends NotifierProviderElement<DownloadProgress, double>
with DownloadProgressRef {
_DownloadProgressProviderElement(super.provider);
class _ItemDownloadProgressProviderElement
extends AutoDisposeAsyncNotifierProviderElement<ItemDownloadProgress,
double?> with ItemDownloadProgressRef {
_ItemDownloadProgressProviderElement(super.provider);
@override
String get id => (origin as DownloadProgressProvider).id;
String get id => (origin as ItemDownloadProgressProvider).id;
}
String _$downloadStatusHash() => r'3f2bf4e7fb01b9329d31f4797537a307aff70397';
String _$isItemDownloadedHash() => r'9bb7ba28bdb73e1ba706e849fedc9c7bd67f4b67';
abstract class _$DownloadStatus
abstract class _$IsItemDownloaded
extends BuildlessAutoDisposeAsyncNotifier<bool> {
late final LibraryItemExpanded item;
@ -485,27 +488,27 @@ abstract class _$DownloadStatus
);
}
/// See also [DownloadStatus].
@ProviderFor(DownloadStatus)
const downloadStatusProvider = DownloadStatusFamily();
/// See also [IsItemDownloaded].
@ProviderFor(IsItemDownloaded)
const isItemDownloadedProvider = IsItemDownloadedFamily();
/// See also [DownloadStatus].
class DownloadStatusFamily extends Family<AsyncValue<bool>> {
/// See also [DownloadStatus].
const DownloadStatusFamily();
/// See also [IsItemDownloaded].
class IsItemDownloadedFamily extends Family<AsyncValue<bool>> {
/// See also [IsItemDownloaded].
const IsItemDownloadedFamily();
/// See also [DownloadStatus].
DownloadStatusProvider call(
/// 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,
@ -524,30 +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 AutoDisposeAsyncNotifierProviderImpl<DownloadStatus, bool> {
/// See also [DownloadStatus].
DownloadStatusProvider(
/// See also [IsItemDownloaded].
class IsItemDownloadedProvider
extends AutoDisposeAsyncNotifierProviderImpl<IsItemDownloaded, bool> {
/// See also [IsItemDownloaded].
IsItemDownloadedProvider(
LibraryItemExpanded item,
) : this._internal(
() => DownloadStatus()..item = 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,
@ -561,7 +564,7 @@ class DownloadStatusProvider
@override
FutureOr<bool> runNotifierBuild(
covariant DownloadStatus notifier,
covariant IsItemDownloaded notifier,
) {
return notifier.build(
item,
@ -569,10 +572,10 @@ class DownloadStatusProvider
}
@override
Override overrideWith(DownloadStatus Function() create) {
Override overrideWith(IsItemDownloaded Function() create) {
return ProviderOverride(
origin: this,
override: DownloadStatusProvider._internal(
override: IsItemDownloadedProvider._internal(
() => create()..item = item,
from: from,
name: null,
@ -585,14 +588,14 @@ class DownloadStatusProvider
}
@override
AutoDisposeAsyncNotifierProviderElement<DownloadStatus, bool>
AutoDisposeAsyncNotifierProviderElement<IsItemDownloaded, bool>
createElement() {
return _DownloadStatusProviderElement(this);
return _IsItemDownloadedProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is DownloadStatusProvider && other.item == item;
return other is IsItemDownloadedProvider && other.item == item;
}
@override
@ -604,18 +607,18 @@ class DownloadStatusProvider
}
}
mixin DownloadStatusRef on AutoDisposeAsyncNotifierProviderRef<bool> {
mixin IsItemDownloadedRef on AutoDisposeAsyncNotifierProviderRef<bool> {
/// The parameter `item` of this provider.
LibraryItemExpanded get item;
}
class _DownloadStatusProviderElement
extends AutoDisposeAsyncNotifierProviderElement<DownloadStatus, 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;
}
// 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,7 +1,6 @@
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;
@ -11,9 +10,9 @@ import 'package:vaani/features/downloads/providers/download_manager.dart'
show
downloadHistoryProvider,
downloadManagerProvider,
downloadProgressProvider,
downloadStatusProvider,
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';
@ -214,12 +213,13 @@ class LibItemDownloadButton extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final isItemDownloaded = ref.watch(downloadStatusProvider(item));
final isItemDownloaded = ref.watch(isItemDownloadedProvider(item));
if (isItemDownloaded.valueOrNull ?? false) {
return AlreadyItemDownloadedButton(item: item);
}
final isItemDownloading = ref.watch(isItemDownloadingProvider(item.id));
return isItemDownloaded.valueOrNull ?? false
? AlreadyItemDownloadedButton(item: item)
: isItemDownloading
return isItemDownloading
? ItemCurrentlyInDownloadQueue(
item: item,
)
@ -248,9 +248,14 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final progress = ref.watch(downloadProgressProvider(item.id));
final updatesStream = useStream(ref.watch(downloadManagerProvider).taskUpdateStream);
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: [
@ -260,19 +265,22 @@ class ItemCurrentlyInDownloadQueue extends HookConsumerWidget {
),
const Icon(
Icons.download,
// color: Theme.of(context).progressIndicatorTheme.color,
)
.animate(
onPlay: (controller) => controller.repeat(),
)
.fade(
begin: 0.5,
duration: shimmerDuration,
end: 1,
duration: 1.seconds,
begin: 0.6,
curve: Curves.linearToEaseOut,
)
.fade(
duration: shimmerDuration,
end: 0.7,
begin: 1,
end: 0.5,
duration: 1.seconds,
curve: Curves.easeInToLinear,
),
],
);
@ -520,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

@ -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,
);
}
}