diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0232257..626f03d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1238,5 +1238,9 @@ }, "newUpdateFoundOnGithub": "Found a new update on Github", "enableBackgroundPostersTitle": "Enable background posters", - "enableBackgroundPostersDesc": "Show random posters in applicable screens" + "enableBackgroundPostersDesc": "Show random posters in applicable screens", + "notificationDownloadingDownloading": "Downloading", + "notificationDownloadingPaused": "Download paused", + "notificationDownloadingFinished": "Download finished", + "notificationDownloadingError": "Download error" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e21ea46..465bd0c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -297,6 +297,7 @@ class _MainState extends ConsumerState
with WindowListener, WidgetsBinding }, builder: (context, child) => LocalizationContextWrapper( child: ScaffoldMessenger(child: child ?? Container()), + currentLocale: language, ), debugShowCheckedModeBanner: false, darkTheme: darkTheme.copyWith( diff --git a/lib/models/credentials_model.dart b/lib/models/credentials_model.dart index bf3eea6..e64026b 100644 --- a/lib/models/credentials_model.dart +++ b/lib/models/credentials_model.dart @@ -26,7 +26,6 @@ class CredentialsModel { Map header(Ref ref) { final application = ref.read(applicationInfoProvider); final headers = { - 'content-type': 'application/json', 'authorization': 'MediaBrowser Token="$token", Client="${application.name}", Device="${application.os}", DeviceId="$deviceId", Version="${application.version}"' }; diff --git a/lib/models/items/season_model.dart b/lib/models/items/season_model.dart index 8101826..7cfed17 100644 --- a/lib/models/items/season_model.dart +++ b/lib/models/items/season_model.dart @@ -74,6 +74,9 @@ class SeasonModel extends ItemBaseModel with SeasonModelMappable { episodes.firstWhereOrNull((element) => element.userData.played == false); } + @override + bool get syncAble => true; + @override ImagesData? get getPosters => images ?? parentImages; diff --git a/lib/models/syncing/i_synced_item.dart b/lib/models/syncing/i_synced_item.dart index 7c82fa8..51db03e 100644 --- a/lib/models/syncing/i_synced_item.dart +++ b/lib/models/syncing/i_synced_item.dart @@ -30,6 +30,7 @@ class ISyncedItem { String id; bool syncing; String? sortName; + @Index() String? parentId; String? path; int? fileSize; diff --git a/lib/models/syncing/i_synced_item.g.dart b/lib/models/syncing/i_synced_item.g.dart index e7f3285..ad46c6d 100644 --- a/lib/models/syncing/i_synced_item.g.dart +++ b/lib/models/syncing/i_synced_item.g.dart @@ -77,7 +77,16 @@ const ISyncedItemSchema = IsarGeneratedSchema( type: IsarType.string, ), ], - indexes: [], + indexes: [ + IsarIndexSchema( + name: 'parentId', + properties: [ + "parentId", + ], + unique: false, + hash: false, + ), + ], ), converter: IsarObjectConverter( serialize: serializeISyncedItem, diff --git a/lib/models/syncing/sync_item.dart b/lib/models/syncing/sync_item.dart index d87906c..0d28345 100644 --- a/lib/models/syncing/sync_item.dart +++ b/lib/models/syncing/sync_item.dart @@ -47,6 +47,7 @@ class SyncedItem with _$SyncedItem { }) = _SyncItem; static String trickPlayPath = "TrickPlay"; + static String chaptersPath = "Chapters"; List get chapters => fChapters.map((e) => e.copyWith(imageUrl: joinAll({"$path", e.imageUrl}))).toList(); @@ -94,6 +95,7 @@ class SyncedItem with _$SyncedItem { try { await videoFile.delete(); await Directory(joinAll([directory.path, trickPlayPath])).delete(recursive: true); + await Directory(joinAll([directory.path, chaptersPath])).delete(recursive: true); } catch (e) { return false; } diff --git a/lib/providers/items/series_details_provider.dart b/lib/providers/items/series_details_provider.dart index 61083b0..649ed97 100644 --- a/lib/providers/items/series_details_provider.dart +++ b/lib/providers/items/series_details_provider.dart @@ -32,20 +32,29 @@ class SeriesDetailViewNotifier extends StateNotifier { final response = await api.usersUserIdItemsItemIdGet(itemId: seriesModel.id); if (response.body == null) return null; newState = response.bodyOrThrow as SeriesModel; - final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id); - newState = newState.copyWith(seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref)); final episodes = await api.showsSeriesIdEpisodesGet(seriesId: seriesModel.id, fields: [ ItemFields.mediastreams, ItemFields.mediasources, ItemFields.overview, + ItemFields.candownload, ]); + final newEpisodes = EpisodeModel.episodesFromDto( + episodes.body?.items, + ref, + ); + final episodesCanDownload = newEpisodes.any((episode) => episode.canDownload == true); + final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id); newState = newState.copyWith( - availableEpisodes: EpisodeModel.episodesFromDto( - episodes.body?.items, - ref, - ), + seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref) + .map((element) => element.copyWith(canDownload: true)) + .toList(), + ); + + newState = newState.copyWith( + canDownload: episodesCanDownload, + availableEpisodes: newEpisodes, ); final related = await ref.read(relatedUtilityProvider).relatedContent(seriesModel.id); diff --git a/lib/providers/sync/background_download_provider.dart b/lib/providers/sync/background_download_provider.dart index 9b30cd6..89cac1f 100644 --- a/lib/providers/sync/background_download_provider.dart +++ b/lib/providers/sync/background_download_provider.dart @@ -1,7 +1,10 @@ +import 'package:flutter/widgets.dart'; + import 'package:background_downloader/background_downloader.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; +import 'package:fladder/util/localization_helper.dart'; part 'background_download_provider.g.dart'; @@ -14,13 +17,7 @@ class BackgroundDownloader extends _$BackgroundDownloader { ..configure( globalConfig: globalConfig(maxDownloads), ) - ..trackTasks() - ..configureNotification( - running: const TaskNotification('Downloading', 'file: {filename}'), - complete: const TaskNotification('Download finished', 'file: {filename}'), - paused: const TaskNotification('Download paused', 'file: {filename}'), - progressBar: true, - ); + ..trackTasks(); } void setMaxConcurrent(int value) { @@ -29,6 +26,16 @@ class BackgroundDownloader extends _$BackgroundDownloader { ); } + void updateTranslations(BuildContext context) async { + state.configureNotification( + running: TaskNotification(context.localized.notificationDownloadingDownloading, '{filename}\n{networkSpeed}'), + complete: TaskNotification(context.localized.notificationDownloadingFinished, '{filename}'), + paused: TaskNotification(context.localized.notificationDownloadingPaused, '{filename}'), + error: TaskNotification(context.localized.notificationDownloadingError, '{filename}'), + progressBar: true, + ); + } + (String, dynamic) globalConfig(int value) => value == 0 ? ("", "") : ( diff --git a/lib/providers/sync/background_download_provider.g.dart b/lib/providers/sync/background_download_provider.g.dart index 285aa71..9a0a63e 100644 --- a/lib/providers/sync/background_download_provider.g.dart +++ b/lib/providers/sync/background_download_provider.g.dart @@ -7,7 +7,7 @@ part of 'background_download_provider.dart'; // ************************************************************************** String _$backgroundDownloaderHash() => - r'dc27f708fc2f1695d37afcb99f8814bc024037af'; + r'9d866549ed7632e855ba30de2765368960889cff'; /// See also [BackgroundDownloader]. @ProviderFor(BackgroundDownloader) diff --git a/lib/providers/sync/sync_provider_helpers.dart b/lib/providers/sync/sync_provider_helpers.dart index 7bc06af..8dde6ed 100644 --- a/lib/providers/sync/sync_provider_helpers.dart +++ b/lib/providers/sync/sync_provider_helpers.dart @@ -11,30 +11,40 @@ part 'sync_provider_helpers.g.dart'; @riverpod class SyncChildren extends _$SyncChildren { @override - List build(SyncedItem arg) { - final syncedItemIsar = ref.watch(syncProvider.notifier).isar; - final allChildren = []; - List toProcess = [arg]; + List build(SyncedItem root) { + final isar = ref.watch(syncProvider.notifier).isar; + final syncPath = ref.read(syncProvider.notifier).syncPath ?? ""; + + if (isar == null) return []; + + final all = []; + List toProcess = [root]; + while (toProcess.isNotEmpty) { - final currentLevel = toProcess.map( - (parent) { - final children = syncedItemIsar?.iSyncedItems.where().parentIdEqualTo(parent.id).sortBySortName().findAll(); - return children?.map((e) => SyncedItem.fromIsar(e, ref.read(syncProvider.notifier).syncPath ?? "")) ?? - []; - }, - ); - allChildren.addAll(currentLevel.expand((list) => list)); - toProcess = currentLevel.expand((list) => list).toList(); + final parentIds = toProcess.map((e) => e.id).toList(); + + final children = []; + for (final id in parentIds) { + final results = isar.iSyncedItems.where().parentIdEqualTo(id).sortBySortName().findAll(); + children.addAll(results); + } + + if (children.isEmpty) break; + + final wrapped = children.map((e) => SyncedItem.fromIsar(e, syncPath)).toList(); + all.addAll(wrapped); + toProcess = wrapped; } - return allChildren; + + return all; } } @riverpod class SyncDownloadStatus extends _$SyncDownloadStatus { @override - DownloadStream? build(SyncedItem arg) { - final nestedChildren = ref.watch(syncChildrenProvider(arg)); + DownloadStream? build(SyncedItem arg, List children) { + final nestedChildren = children; ref.watch(downloadTasksProvider(arg.id)); for (var element in nestedChildren) { @@ -64,20 +74,23 @@ class SyncDownloadStatus extends _$SyncDownloadStatus { @riverpod class SyncStatuses extends _$SyncStatuses { @override - FutureOr build(SyncedItem arg) async { - final nestedChildren = ref.watch(syncChildrenProvider(arg)); + FutureOr build(SyncedItem arg, List? children) async { + final nestedChildren = children; ref.watch(downloadTasksProvider(arg.id)); - for (var element in nestedChildren) { - ref.watch(downloadTasksProvider(element.id)); - } + if (nestedChildren != null) { + for (var element in nestedChildren) { + ref.watch(downloadTasksProvider(element.id)); + } - for (var i = 0; i < nestedChildren.length; i++) { - final item = nestedChildren[i]; - if (item.hasVideoFile && !await item.videoFile.exists()) { - return SyncStatus.partially; + for (var i = 0; i < nestedChildren.length; i++) { + final item = nestedChildren[i]; + if (item.hasVideoFile && !await item.videoFile.exists()) { + return SyncStatus.partially; + } } } + if (arg.hasVideoFile && !await arg.videoFile.exists()) { return SyncStatus.partially; } @@ -88,17 +101,21 @@ class SyncStatuses extends _$SyncStatuses { @riverpod class SyncSize extends _$SyncSize { @override - int? build(SyncedItem arg) { - final nestedChildren = ref.watch(syncChildrenProvider(arg)); + int? build(SyncedItem arg, List? children) { + final nestedChildren = children; ref.watch(downloadTasksProvider(arg.id)); - for (var element in nestedChildren) { - ref.watch(downloadTasksProvider(element.id)); - } int size = arg.fileSize ?? 0; - for (var element in nestedChildren) { - size += element.fileSize ?? 0; + + if (nestedChildren != null) { + for (var element in nestedChildren) { + ref.watch(downloadTasksProvider(element.id)); + } + for (var element in nestedChildren) { + size += element.fileSize ?? 0; + } } + return size; } } diff --git a/lib/providers/sync/sync_provider_helpers.g.dart b/lib/providers/sync/sync_provider_helpers.g.dart index deb128b..eb64223 100644 --- a/lib/providers/sync/sync_provider_helpers.g.dart +++ b/lib/providers/sync/sync_provider_helpers.g.dart @@ -6,7 +6,7 @@ part of 'sync_provider_helpers.dart'; // RiverpodGenerator // ************************************************************************** -String _$syncChildrenHash() => r'f6fdb1aa36d6655976baa5fbe0d8a6b812d7e95b'; +String _$syncChildrenHash() => r'c5a90d630d49f59ad4fbaacb5154f1205799f5ab'; /// Copied from Dart SDK class _SystemHash { @@ -31,10 +31,10 @@ class _SystemHash { abstract class _$SyncChildren extends BuildlessAutoDisposeNotifier> { - late final SyncedItem arg; + late final SyncedItem root; List build( - SyncedItem arg, + SyncedItem root, ); } @@ -49,10 +49,10 @@ class SyncChildrenFamily extends Family> { /// See also [SyncChildren]. SyncChildrenProvider call( - SyncedItem arg, + SyncedItem root, ) { return SyncChildrenProvider( - arg, + root, ); } @@ -61,7 +61,7 @@ class SyncChildrenFamily extends Family> { covariant SyncChildrenProvider provider, ) { return call( - provider.arg, + provider.root, ); } @@ -85,9 +85,9 @@ class SyncChildrenProvider extends AutoDisposeNotifierProviderImpl> { /// See also [SyncChildren]. SyncChildrenProvider( - SyncedItem arg, + SyncedItem root, ) : this._internal( - () => SyncChildren()..arg = arg, + () => SyncChildren()..root = root, from: syncChildrenProvider, name: r'syncChildrenProvider', debugGetCreateSourceHash: @@ -97,7 +97,7 @@ class SyncChildrenProvider dependencies: SyncChildrenFamily._dependencies, allTransitiveDependencies: SyncChildrenFamily._allTransitiveDependencies, - arg: arg, + root: root, ); SyncChildrenProvider._internal( @@ -107,17 +107,17 @@ class SyncChildrenProvider required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, - required this.arg, + required this.root, }) : super.internal(); - final SyncedItem arg; + final SyncedItem root; @override List runNotifierBuild( covariant SyncChildren notifier, ) { return notifier.build( - arg, + root, ); } @@ -126,13 +126,13 @@ class SyncChildrenProvider return ProviderOverride( origin: this, override: SyncChildrenProvider._internal( - () => create()..arg = arg, + () => create()..root = root, from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, - arg: arg, + root: root, ), ); } @@ -145,13 +145,13 @@ class SyncChildrenProvider @override bool operator ==(Object other) { - return other is SyncChildrenProvider && other.arg == arg; + return other is SyncChildrenProvider && other.root == root; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, arg.hashCode); + hash = _SystemHash.combine(hash, root.hashCode); return _SystemHash.finish(hash); } @@ -160,8 +160,8 @@ class SyncChildrenProvider @Deprecated('Will be removed in 3.0. Use Ref instead') // ignore: unused_element mixin SyncChildrenRef on AutoDisposeNotifierProviderRef> { - /// The parameter `arg` of this provider. - SyncedItem get arg; + /// The parameter `root` of this provider. + SyncedItem get root; } class _SyncChildrenProviderElement @@ -170,18 +170,20 @@ class _SyncChildrenProviderElement _SyncChildrenProviderElement(super.provider); @override - SyncedItem get arg => (origin as SyncChildrenProvider).arg; + SyncedItem get root => (origin as SyncChildrenProvider).root; } String _$syncDownloadStatusHash() => - r'5a0f8537a977c52e6083bd84265631ea5d160637'; + r'1036352200e1138b4ef70e524c0baf13bb9cd452'; abstract class _$SyncDownloadStatus extends BuildlessAutoDisposeNotifier { late final SyncedItem arg; + late final List children; DownloadStream? build( SyncedItem arg, + List children, ); } @@ -197,9 +199,11 @@ class SyncDownloadStatusFamily extends Family { /// See also [SyncDownloadStatus]. SyncDownloadStatusProvider call( SyncedItem arg, + List children, ) { return SyncDownloadStatusProvider( arg, + children, ); } @@ -209,6 +213,7 @@ class SyncDownloadStatusFamily extends Family { ) { return call( provider.arg, + provider.children, ); } @@ -233,8 +238,11 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< /// See also [SyncDownloadStatus]. SyncDownloadStatusProvider( SyncedItem arg, + List children, ) : this._internal( - () => SyncDownloadStatus()..arg = arg, + () => SyncDownloadStatus() + ..arg = arg + ..children = children, from: syncDownloadStatusProvider, name: r'syncDownloadStatusProvider', debugGetCreateSourceHash: @@ -245,6 +253,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< allTransitiveDependencies: SyncDownloadStatusFamily._allTransitiveDependencies, arg: arg, + children: children, ); SyncDownloadStatusProvider._internal( @@ -255,9 +264,11 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< required super.debugGetCreateSourceHash, required super.from, required this.arg, + required this.children, }) : super.internal(); final SyncedItem arg; + final List children; @override DownloadStream? runNotifierBuild( @@ -265,6 +276,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< ) { return notifier.build( arg, + children, ); } @@ -273,13 +285,16 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< return ProviderOverride( origin: this, override: SyncDownloadStatusProvider._internal( - () => create()..arg = arg, + () => create() + ..arg = arg + ..children = children, from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, arg: arg, + children: children, ), ); } @@ -292,13 +307,16 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< @override bool operator ==(Object other) { - return other is SyncDownloadStatusProvider && other.arg == arg; + return other is SyncDownloadStatusProvider && + other.arg == arg && + other.children == children; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, arg.hashCode); + hash = _SystemHash.combine(hash, children.hashCode); return _SystemHash.finish(hash); } @@ -309,6 +327,9 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< mixin SyncDownloadStatusRef on AutoDisposeNotifierProviderRef { /// The parameter `arg` of this provider. SyncedItem get arg; + + /// The parameter `children` of this provider. + List get children; } class _SyncDownloadStatusProviderElement @@ -318,16 +339,21 @@ class _SyncDownloadStatusProviderElement @override SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg; + @override + List get children => + (origin as SyncDownloadStatusProvider).children; } -String _$syncStatusesHash() => r'f05ee53368d1de130714bba09132e08aba15bc44'; +String _$syncStatusesHash() => r'64a3499fc7b7bbdbd6594b1eec76cf42a119a041'; abstract class _$SyncStatuses extends BuildlessAutoDisposeAsyncNotifier { late final SyncedItem arg; + late final List? children; FutureOr build( SyncedItem arg, + List? children, ); } @@ -343,9 +369,11 @@ class SyncStatusesFamily extends Family> { /// See also [SyncStatuses]. SyncStatusesProvider call( SyncedItem arg, + List? children, ) { return SyncStatusesProvider( arg, + children, ); } @@ -355,6 +383,7 @@ class SyncStatusesFamily extends Family> { ) { return call( provider.arg, + provider.children, ); } @@ -379,8 +408,11 @@ class SyncStatusesProvider /// See also [SyncStatuses]. SyncStatusesProvider( SyncedItem arg, + List? children, ) : this._internal( - () => SyncStatuses()..arg = arg, + () => SyncStatuses() + ..arg = arg + ..children = children, from: syncStatusesProvider, name: r'syncStatusesProvider', debugGetCreateSourceHash: @@ -391,6 +423,7 @@ class SyncStatusesProvider allTransitiveDependencies: SyncStatusesFamily._allTransitiveDependencies, arg: arg, + children: children, ); SyncStatusesProvider._internal( @@ -401,9 +434,11 @@ class SyncStatusesProvider required super.debugGetCreateSourceHash, required super.from, required this.arg, + required this.children, }) : super.internal(); final SyncedItem arg; + final List? children; @override FutureOr runNotifierBuild( @@ -411,6 +446,7 @@ class SyncStatusesProvider ) { return notifier.build( arg, + children, ); } @@ -419,13 +455,16 @@ class SyncStatusesProvider return ProviderOverride( origin: this, override: SyncStatusesProvider._internal( - () => create()..arg = arg, + () => create() + ..arg = arg + ..children = children, from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, arg: arg, + children: children, ), ); } @@ -438,13 +477,16 @@ class SyncStatusesProvider @override bool operator ==(Object other) { - return other is SyncStatusesProvider && other.arg == arg; + return other is SyncStatusesProvider && + other.arg == arg && + other.children == children; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, arg.hashCode); + hash = _SystemHash.combine(hash, children.hashCode); return _SystemHash.finish(hash); } @@ -455,6 +497,9 @@ class SyncStatusesProvider mixin SyncStatusesRef on AutoDisposeAsyncNotifierProviderRef { /// The parameter `arg` of this provider. SyncedItem get arg; + + /// The parameter `children` of this provider. + List? get children; } class _SyncStatusesProviderElement @@ -464,15 +509,19 @@ class _SyncStatusesProviderElement @override SyncedItem get arg => (origin as SyncStatusesProvider).arg; + @override + List? get children => (origin as SyncStatusesProvider).children; } -String _$syncSizeHash() => r'138702f2dd69ab28d142bab67ab4a497bb24f252'; +String _$syncSizeHash() => r'81797ecc4a6f600691b6f1fe0c16bae0228ec920'; abstract class _$SyncSize extends BuildlessAutoDisposeNotifier { late final SyncedItem arg; + late final List? children; int? build( SyncedItem arg, + List? children, ); } @@ -488,9 +537,11 @@ class SyncSizeFamily extends Family { /// See also [SyncSize]. SyncSizeProvider call( SyncedItem arg, + List? children, ) { return SyncSizeProvider( arg, + children, ); } @@ -500,6 +551,7 @@ class SyncSizeFamily extends Family { ) { return call( provider.arg, + provider.children, ); } @@ -523,8 +575,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { /// See also [SyncSize]. SyncSizeProvider( SyncedItem arg, + List? children, ) : this._internal( - () => SyncSize()..arg = arg, + () => SyncSize() + ..arg = arg + ..children = children, from: syncSizeProvider, name: r'syncSizeProvider', debugGetCreateSourceHash: @@ -534,6 +589,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { dependencies: SyncSizeFamily._dependencies, allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies, arg: arg, + children: children, ); SyncSizeProvider._internal( @@ -544,9 +600,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { required super.debugGetCreateSourceHash, required super.from, required this.arg, + required this.children, }) : super.internal(); final SyncedItem arg; + final List? children; @override int? runNotifierBuild( @@ -554,6 +612,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { ) { return notifier.build( arg, + children, ); } @@ -562,13 +621,16 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { return ProviderOverride( origin: this, override: SyncSizeProvider._internal( - () => create()..arg = arg, + () => create() + ..arg = arg + ..children = children, from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, arg: arg, + children: children, ), ); } @@ -580,13 +642,16 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { @override bool operator ==(Object other) { - return other is SyncSizeProvider && other.arg == arg; + return other is SyncSizeProvider && + other.arg == arg && + other.children == children; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); hash = _SystemHash.combine(hash, arg.hashCode); + hash = _SystemHash.combine(hash, children.hashCode); return _SystemHash.finish(hash); } @@ -597,6 +662,9 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { mixin SyncSizeRef on AutoDisposeNotifierProviderRef { /// The parameter `arg` of this provider. SyncedItem get arg; + + /// The parameter `children` of this provider. + List? get children; } class _SyncSizeProviderElement @@ -606,6 +674,8 @@ class _SyncSizeProviderElement @override SyncedItem get arg => (origin as SyncSizeProvider).arg; + @override + List? get children => (origin as SyncSizeProvider).children; } // ignore_for_file: type=lint // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package diff --git a/lib/providers/sync_provider.dart b/lib/providers/sync_provider.dart index 2bb32d1..522e0f1 100644 --- a/lib/providers/sync_provider.dart +++ b/lib/providers/sync_provider.dart @@ -22,6 +22,7 @@ import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/movie_model.dart'; +import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/models/syncing/download_stream.dart'; @@ -238,6 +239,7 @@ class SyncNotifier extends StateNotifier { fladderSnackbar(context, title: context.localized.syncAddItemForSyncing(item.detailedName(context) ?? "Unknown")); final newSync = switch (item) { EpisodeModel episode => await syncSeries(item.parentBaseModel, episode: episode), + SeasonModel season => await syncSeries(item.parentBaseModel, season: season), SeriesModel series => await syncSeries(series), MovieModel movie => await syncMovie(movie), _ => null @@ -367,7 +369,7 @@ class SyncNotifier extends StateNotifier { if (data == null) return data; if (!itemPath.existsSync()) return data; if (data.isEmpty) return data; - final saveDirectory = Directory(path.joinAll([itemPath.path, "Chapters"])); + final saveDirectory = Directory(path.joinAll([itemPath.path, SyncedItem.chaptersPath])); await saveDirectory.create(recursive: true); @@ -378,7 +380,7 @@ class SyncNotifier extends StateNotifier { if (response.bodyBytes.isEmpty) return null; file.writeAsBytesSync(response.bodyBytes); return event.copyWith( - imageUrl: path.joinAll(["Chapters", fileName]), + imageUrl: path.joinAll([SyncedItem.chaptersPath, fileName]), ); }).toList(); return saveChapters.nonNulls.toList(); @@ -415,7 +417,7 @@ class SyncNotifier extends StateNotifier { return syncedItem; } - Future syncVideoFile(SyncedItem syncItem, bool skipDownload) async { + Future syncFile(SyncedItem syncItem, bool skipDownload) async { cleanupTemporaryFiles(); final playbackResponse = await api.itemsItemIdPlaybackInfoPost( @@ -439,6 +441,7 @@ class SyncNotifier extends StateNotifier { final mediaSegments = (await api.mediaSegmentsGet(id: syncItem.id))?.body; syncItem = syncItem.copyWith( + fChapters: await saveChapterImages(item?.overview.chapters, directory) ?? [], subtitles: subtitles, fTrickPlayModel: trickPlayFile, mediaSegments: mediaSegments, @@ -447,23 +450,24 @@ class SyncNotifier extends StateNotifier { await updateItem(syncItem); final currentTask = ref.read(downloadTasksProvider(syncItem.id)); + final user = ref.read(userProvider); - final downloadString = path.joinAll([ - "${ref.read(userProvider)?.server}", - "Items", - "${syncItem.id}/Download?api_key=${ref.read(userProvider)?.credentials.token}" - ]); + if (user == null) return null; + + final downloadUrl = path.joinAll([user.server, "Items", syncItem.id, "Download"]); try { if (!skipDownload && currentTask.task == null) { final downloadTask = DownloadTask( - url: Uri.parse(downloadString).toString(), + url: Uri.parse(downloadUrl).toString(), directory: syncItem.directory.path, filename: syncItem.videoFileName, updates: Updates.statusAndProgress, baseDirectory: BaseDirectory.root, + urlQueryParameters: {"api_key": user.credentials.token}, + headers: user.credentials.header(ref), requiresWiFi: ref.read(clientSettingsProvider.select((value) => value.requireWifi)), - retries: 5, + retries: 3, allowPause: true, ); @@ -490,7 +494,7 @@ class SyncNotifier extends StateNotifier { (state) => state.copyWith(status: status), ); - if (status == TaskStatus.complete) { + if (status == TaskStatus.complete || status == TaskStatus.canceled) { ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => DownloadStream.empty()); } }, @@ -522,6 +526,10 @@ extension SyncNotifierHelpers on SyncNotifier { Future createSyncItem(BaseItemDto response, {SyncedItem? parent}) async { final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref); + final existingSyncedItem = getSyncedItem(item); + + if (existingSyncedItem != null) return existingSyncedItem; + final Directory? parentDirectory = parent?.directory; final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id])); @@ -543,31 +551,17 @@ extension SyncNotifierHelpers on SyncNotifier { userData: item.userData, ); - //Save item if parent so the user is aware. if (parent == null) { isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath))); } - final origChapters = Chapter.chaptersFromInfo(item.id, response.chapters ?? [], ref); - return syncItem.copyWith( - fChapters: await saveChapterImages(origChapters, directory) ?? [], fileSize: response.mediaSources?.firstOrNull?.size ?? 0, syncing: false, videoFileName: response.path?.split('/').lastOrNull ?? "", ); } - // Need to move the file after downloading on Android - Future moveFile(DownloadTask downloadTask, SyncedItem syncItem) async { - final currentLocation = File(await downloadTask.filePath()); - final wantedLocation = syncItem.videoFile; - if (currentLocation.path != wantedLocation.path) { - await currentLocation.copy(wantedLocation.path); - await currentLocation.delete(); - } - } - Future syncMovie(ItemBaseModel item, {bool skipDownload = false}) async { final response = await api.usersUserIdItemsItemIdGetBaseItem( itemId: item.id, @@ -580,21 +574,21 @@ extension SyncNotifierHelpers on SyncNotifier { if (!syncItem.directory.existsSync()) return null; - await syncVideoFile(syncItem, skipDownload); + await syncFile(syncItem, skipDownload); isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath))); return syncItem; } - Future syncSeries(SeriesModel item, {EpisodeModel? episode}) async { + Future syncSeries(SeriesModel item, {SeasonModel? season, EpisodeModel? episode}) async { final response = await api.usersUserIdItemsItemIdGetBaseItem( itemId: item.id, ); List newItems = []; - SyncedItem? itemToDownload; + List? itemsToDownload = []; SyncedItem seriesItem = await createSyncItem(response.bodyOrThrow); newItems.add(seriesItem); @@ -627,8 +621,8 @@ extension SyncNotifierHelpers on SyncNotifier { final seasons = seasonsResponse.body?.items ?? []; for (var i = 0; i < seasons.length; i++) { - final season = seasons[i]; - final syncedSeason = await createSyncItem(season, parent: seriesItem); + final newSeason = seasons[i]; + final syncedSeason = await createSyncItem(newSeason, parent: seriesItem); newItems.add(syncedSeason); final episodesResponse = await api.showsSeriesIdEpisodesGet( isMissing: false, @@ -651,16 +645,23 @@ extension SyncNotifierHelpers on SyncNotifier { ItemFields.chapters, ItemFields.trickplay, ], - seasonId: season.id, + seasonId: newSeason.id, seriesId: seriesItem.id, ); + final episodes = episodesResponse.body?.items ?? []; - for (var i = 0; i < episodes.length; i++) { - final item = episodes[i]; - final newEpisode = await createSyncItem(item, parent: syncedSeason); + + final episodeResults = await Future.wait( + episodes.map((ep) async { + final newEpisode = await createSyncItem(ep, parent: syncedSeason); + return (ep, newEpisode); + }), + ); + + for (final (ep, newEpisode) in episodeResults) { newItems.add(newEpisode); - if (episode?.id == item.id) { - itemToDownload = newEpisode; + if (episode?.id == ep.id || newSeason.id == season?.id) { + itemsToDownload.add(newEpisode); } } } @@ -673,8 +674,9 @@ extension SyncNotifierHelpers on SyncNotifier { .toList()), ); - if (itemToDownload != null) { - await syncVideoFile(itemToDownload, false); + for (var i = 0; i < itemsToDownload.length; i++) { + final item = itemsToDownload[i]; + await syncFile(item, false); } return seriesItem; diff --git a/lib/routes/auto_router.dart b/lib/routes/auto_router.dart index a3f885c..9bca7e9 100644 --- a/lib/routes/auto_router.dart +++ b/lib/routes/auto_router.dart @@ -71,22 +71,26 @@ final AutoRoute _dashboardRoute = CustomRoute( page: DashboardRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, initial: true, + maintainState: false, path: 'dashboard', ); final AutoRoute _favouritesRoute = CustomRoute( page: FavouritesRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, + maintainState: false, path: 'favourites', ); final AutoRoute _syncedRoute = CustomRoute( page: SyncedRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, + maintainState: false, path: 'synced', ); final AutoRoute _librariesRoute = CustomRoute( page: LibraryRoute.page, transitionsBuilder: TransitionsBuilders.fadeIn, + maintainState: false, path: 'libraries', ); diff --git a/lib/screens/shared/media/episode_posters.dart b/lib/screens/shared/media/episode_posters.dart index 653a034..0c547ad 100644 --- a/lib/screens/shared/media/episode_posters.dart +++ b/lib/screens/shared/media/episode_posters.dart @@ -206,7 +206,7 @@ class EpisodePoster extends ConsumerWidget { if (iSyncedItem != null) Consumer(builder: (context, ref, child) { final SyncStatus syncStatus = - ref.watch(syncStatusesProvider(iSyncedItem)).value ?? SyncStatus.partially; + ref.watch(syncStatusesProvider(iSyncedItem, null)).value ?? SyncStatus.partially; return StatusCard( color: syncStatus.color, child: SyncButton(item: episode, syncedItem: syncedItem), diff --git a/lib/screens/shared/media/season_row.dart b/lib/screens/shared/media/season_row.dart index 375692f..ca827f4 100644 --- a/lib/screens/shared/media/season_row.dart +++ b/lib/screens/shared/media/season_row.dart @@ -3,7 +3,9 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/season_model.dart'; +import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/flat_button.dart'; +import 'package:fladder/screens/syncing/sync_button.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/fladder_image.dart'; @@ -56,6 +58,7 @@ class SeasonPoster extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { + final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(season); Padding placeHolder(String title) { return Padding( padding: const EdgeInsets.all(4), @@ -100,15 +103,27 @@ class SeasonPoster extends ConsumerWidget { if (season.userData.unPlayedItemCount != 0) Align( alignment: Alignment.topRight, - child: StatusCard( - color: Theme.of(context).colorScheme.primary, - useFittedBox: true, - child: Center( - child: Text( - season.userData.unPlayedItemCount.toString(), - style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14), + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + if (syncedItem != null) + StatusCard( + child: SyncButton( + item: season, + syncedItem: syncedItem, + ), + ), + StatusCard( + color: Theme.of(context).colorScheme.primary, + useFittedBox: true, + child: Center( + child: Text( + season.userData.unPlayedItemCount.toString(), + style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14), + ), + ), ), - ), + ], ), ) else diff --git a/lib/screens/syncing/sync_button.dart b/lib/screens/syncing/sync_button.dart index ee44de4..6c47ca3 100644 --- a/lib/screens/syncing/sync_button.dart +++ b/lib/screens/syncing/sync_button.dart @@ -1,12 +1,11 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; + import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart'; -import 'package:fladder/providers/sync_provider.dart'; -import 'package:fladder/screens/shared/default_alert_dialog.dart'; -import 'package:fladder/screens/syncing/sync_item_details.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class SyncButton extends ConsumerStatefulWidget { final ItemBaseModel item; @@ -21,37 +20,21 @@ class _SyncButtonState extends ConsumerState { @override Widget build(BuildContext context) { final syncedItem = widget.syncedItem; - final status = syncedItem != null ? ref.watch(syncStatusesProvider(syncedItem)).value : null; - final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem)) : null; + final status = syncedItem != null ? ref.watch(syncStatusesProvider(syncedItem, null)).value : null; + final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem, [])) : null; return Stack( alignment: Alignment.center, children: [ - InkWell( - onTap: syncedItem != null - ? () => showSyncItemDetails(context, syncedItem, ref) - : () => showDefaultActionDialog( - context, - 'Sync ${widget.item.detailedName}?', - null, - (context) async { - await ref.read(syncProvider.notifier).addSyncItem(context, widget.item); - Navigator.of(context).pop(); - }, - "Sync", - (context) => Navigator.of(context).pop(), - "Cancel", - ), - child: Icon( - syncedItem != null - ? status == SyncStatus.partially - ? (progress?.progress ?? 0) > 0 - ? IconsaxPlusLinear.arrow_down - : IconsaxPlusLinear.more_circle - : IconsaxPlusLinear.tick_circle - : IconsaxPlusLinear.arrow_down_2, - color: status?.color, - size: (progress?.progress ?? 0) > 0 ? 16 : null, - ), + Icon( + syncedItem != null + ? status == SyncStatus.partially + ? (progress?.progress ?? 0) > 0 + ? IconsaxPlusLinear.arrow_down + : IconsaxPlusLinear.more_circle + : IconsaxPlusLinear.tick_circle + : IconsaxPlusLinear.arrow_down_2, + color: status?.color, + size: (progress?.progress ?? 0) > 0 ? 16 : null, ), if ((progress?.progress ?? 0) > 0) IgnorePointer( diff --git a/lib/screens/syncing/sync_child_item.dart b/lib/screens/syncing/sync_child_item.dart index 342bb11..bf91518 100644 --- a/lib/screens/syncing/sync_child_item.dart +++ b/lib/screens/syncing/sync_child_item.dart @@ -1,11 +1,12 @@ -import 'package:fladder/models/items/season_model.dart'; -import 'package:fladder/models/syncing/sync_item.dart'; -import 'package:fladder/screens/syncing/widgets/synced_season_poster.dart'; import 'package:flutter/material.dart'; + import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/models/items/episode_model.dart'; +import 'package:fladder/models/items/season_model.dart'; +import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync_provider.dart'; +import 'package:fladder/screens/syncing/widgets/synced_season_poster.dart'; import 'widgets/synced_episode_item.dart'; @@ -33,31 +34,25 @@ class _ChildSyncWidgetState extends ConsumerState { return Padding( padding: const EdgeInsets.symmetric(vertical: 6), child: Card( - child: InkWell( - onTap: () { - Navigator.of(context).pop(); - baseItem.navigateTo(context); - }, - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - children: [ - Flexible( - child: switch (baseItem) { - SeasonModel season => SyncedSeasonPoster( - syncedItem: syncedItem, - season: season, - ), - EpisodeModel episode => SyncedEpisodeItem( - episode: episode, - syncedItem: syncedItem, - hasFile: hasFile, - ), - _ => Container(), - }, - ), - ], - ), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + children: [ + Flexible( + child: switch (baseItem) { + SeasonModel season => SyncedSeasonPoster( + syncedItem: syncedItem, + season: season, + ), + EpisodeModel episode => SyncedEpisodeItem( + episode: episode, + syncedItem: syncedItem, + hasFile: hasFile, + ), + _ => Container(), + }, + ), + ], ), ), ), diff --git a/lib/screens/syncing/sync_item_details.dart b/lib/screens/syncing/sync_item_details.dart index 0b4a276..b13a7d9 100644 --- a/lib/screens/syncing/sync_item_details.dart +++ b/lib/screens/syncing/sync_item_details.dart @@ -1,13 +1,11 @@ import 'package:flutter/material.dart'; -import 'package:background_downloader/background_downloader.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; -import 'package:fladder/providers/sync/background_download_provider.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/adaptive_dialog.dart'; @@ -101,6 +99,7 @@ class _SyncItemDetailsState extends ConsumerState { Expanded( child: SyncProgressBuilder( item: syncedItem, + children: syncChildren, builder: (context, combinedStream) { return Row( children: [ @@ -114,52 +113,17 @@ class _SyncItemDetailsState extends ConsumerState { ), SyncSubtitle(syncItem: syncedItem), SyncLabel( - label: context.localized - .totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), - status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, + label: context.localized.totalSize( + ref.watch(syncSizeProvider(syncedItem, syncChildren)).byteFormat ?? '--'), + status: ref.watch(syncStatusesProvider(syncedItem, syncChildren)).value ?? + SyncStatus.partially, ), + if (combinedStream?.task != null && combinedStream != null) ...{ + SyncProgressBar(item: syncedItem, task: combinedStream) + }, ].addInBetween(const SizedBox(height: 8)), ), ), - if (combinedStream?.task != null) ...{ - if (combinedStream?.status != TaskStatus.paused) - IconButton( - onPressed: () => - ref.read(backgroundDownloaderProvider).pause(combinedStream!.task!), - icon: const Icon(IconsaxPlusBold.pause), - ), - if (combinedStream?.status == TaskStatus.paused) ...[ - IconButton( - onPressed: () => - ref.read(backgroundDownloaderProvider).resume(combinedStream!.task!), - icon: const Icon(IconsaxPlusBold.play), - ), - IconButton( - onPressed: () => ref - .read(syncProvider.notifier) - .deleteFullSyncFiles(syncedItem, combinedStream?.task), - icon: const Icon(IconsaxPlusBold.stop), - ), - ], - const SizedBox(width: 16) - }, - if (combinedStream != null && combinedStream.hasDownload) - SizedBox.fromSize( - size: const Size.fromRadius(35), - child: Stack( - fit: StackFit.expand, - alignment: Alignment.center, - children: [ - CircularProgressIndicator( - value: combinedStream.progress, - strokeWidth: 8, - backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0.5), - strokeCap: StrokeCap.round, - color: combinedStream.status.color(context), - ), - Center(child: Text("${((combinedStream.progress) * 100).toStringAsFixed(0)}%")) - ], - )), ], ); }, @@ -167,7 +131,7 @@ class _SyncItemDetailsState extends ConsumerState { ), if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile) IconButtonAwait( - onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false), + onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false), icon: const Icon(IconsaxPlusLinear.cloud_change), ) else if (hasFile) diff --git a/lib/screens/syncing/sync_list_item.dart b/lib/screens/syncing/sync_list_item.dart index c759c29..3acda62 100644 --- a/lib/screens/syncing/sync_list_item.dart +++ b/lib/screens/syncing/sync_list_item.dart @@ -29,6 +29,7 @@ class SyncListItemState extends ConsumerState { Widget build(BuildContext context) { final syncedItem = widget.syncedItem; final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); + final children = ref.watch(syncChildrenProvider(syncedItem)); return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: SyncStatusOverlay( @@ -89,6 +90,7 @@ class SyncListItemState extends ConsumerState { Expanded( child: SyncProgressBuilder( item: syncedItem, + children: children, builder: (context, combinedStream) { return Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -103,13 +105,17 @@ class SyncListItemState extends ConsumerState { ), ), Flexible( - child: SyncSubtitle(syncItem: syncedItem), + child: SyncSubtitle( + syncItem: syncedItem, + children: children, + ), ), Flexible( child: SyncLabel( - label: context.localized - .totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), - status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, + label: context.localized.totalSize( + ref.watch(syncSizeProvider(syncedItem, children)).byteFormat ?? '--'), + status: ref.watch(syncStatusesProvider(syncedItem, children)).value ?? + SyncStatus.partially, ), ), if (combinedStream != null && combinedStream.hasDownload == true) diff --git a/lib/screens/syncing/sync_widgets.dart b/lib/screens/syncing/sync_widgets.dart index f416c36..ee9d2c1 100644 --- a/lib/screens/syncing/sync_widgets.dart +++ b/lib/screens/syncing/sync_widgets.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:background_downloader/background_downloader.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/season_model.dart'; @@ -15,6 +15,13 @@ import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; +const _cancellableStatuses = { + TaskStatus.canceled, + TaskStatus.failed, + TaskStatus.enqueued, + TaskStatus.waitingToRetry, +}; + class SyncLabel extends ConsumerWidget { final String? label; final SyncStatus status; @@ -76,18 +83,24 @@ class SyncProgressBar extends ConsumerWidget { IconButton( onPressed: () => ref.read(backgroundDownloaderProvider).pause(downloadTask), icon: const Icon(IconsaxPlusBold.pause), + ), + if (downloadStatus == TaskStatus.paused) ...[ + IconButton( + onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask), + icon: const Icon(IconsaxPlusBold.play), + ), + IconButton( + onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask), + icon: const Icon(IconsaxPlusBold.stop), ) + ], + if (_cancellableStatuses.contains(downloadStatus)) ...[ + IconButton( + onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask), + icon: const Icon(IconsaxPlusBold.stop), + ), + ], }, - if (downloadStatus == TaskStatus.paused && downloadTask != null) ...[ - IconButton( - onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask), - icon: const Icon(IconsaxPlusBold.play), - ), - IconButton( - onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask), - icon: const Icon(IconsaxPlusBold.stop), - ) - ], ].addInBetween(const SizedBox(width: 8)), ), const SizedBox(width: 6), @@ -98,16 +111,17 @@ class SyncProgressBar extends ConsumerWidget { class SyncSubtitle extends ConsumerWidget { final SyncedItem syncItem; + final List children; const SyncSubtitle({ required this.syncItem, + this.children = const [], super.key, }); @override Widget build(BuildContext context, WidgetRef ref) { final baseItem = ref.read(syncProvider.notifier).getItem(syncItem); - final children = syncItem.nestedChildren(ref); - final syncStatus = ref.watch(syncStatusesProvider(syncItem)).value ?? SyncStatus.partially; + final syncStatus = ref.watch(syncStatusesProvider(syncItem, children)).value ?? SyncStatus.partially; return Container( decoration: BoxDecoration(color: syncStatus.color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)), diff --git a/lib/screens/syncing/widgets/sync_progress_builder.dart b/lib/screens/syncing/widgets/sync_progress_builder.dart index 56e7616..31382f6 100644 --- a/lib/screens/syncing/widgets/sync_progress_builder.dart +++ b/lib/screens/syncing/widgets/sync_progress_builder.dart @@ -1,17 +1,20 @@ +import 'package:flutter/material.dart'; + +import 'package:flutter_riverpod/flutter_riverpod.dart'; + import 'package:fladder/models/syncing/download_stream.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; class SyncProgressBuilder extends ConsumerWidget { final SyncedItem item; + final List children; final Widget Function(BuildContext context, DownloadStream? combinedStream) builder; - const SyncProgressBuilder({required this.item, required this.builder, super.key}); + const SyncProgressBuilder({required this.item, this.children = const [], required this.builder, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final syncStatus = ref.watch(syncDownloadStatusProvider(item)); + final syncStatus = ref.watch(syncDownloadStatusProvider(item, children)); return builder(context, syncStatus); } } diff --git a/lib/screens/syncing/widgets/synced_episode_item.dart b/lib/screens/syncing/widgets/synced_episode_item.dart index 64a2123..b74a91d 100644 --- a/lib/screens/syncing/widgets/synced_episode_item.dart +++ b/lib/screens/syncing/widgets/synced_episode_item.dart @@ -1,13 +1,15 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:auto_route/auto_route.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/default_alert_dialog.dart'; +import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/util/list_padding.dart'; @@ -40,11 +42,15 @@ class _SyncedEpisodeItemState extends ConsumerState { return Row( children: [ - IgnorePointer( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3), + ConstrainedBox( + constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3), + child: FlatButton( + onTap: () { + widget.episode.navigateTo(context); + return context.maybePop(); + }, child: SizedBox( - width: 250, + width: 175, child: EpisodePoster( episode: widget.episode, syncedItem: syncedItem, @@ -87,8 +93,8 @@ class _SyncedEpisodeItemState extends ConsumerState { else Flexible( child: SyncLabel( - label: context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), - status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, + label: context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem, [])).byteFormat ?? '--'), + status: ref.watch(syncStatusesProvider(syncedItem, [])).value ?? SyncStatus.partially, ), ) ], @@ -96,7 +102,7 @@ class _SyncedEpisodeItemState extends ConsumerState { ), if (!hasFile && !downloadTask.hasDownload) IconButtonAwait( - onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false), + onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false), icon: const Icon(IconsaxPlusLinear.cloud_change), ) else if (hasFile) diff --git a/lib/screens/syncing/widgets/synced_season_poster.dart b/lib/screens/syncing/widgets/synced_season_poster.dart index 79ce2b5..d8afbf0 100644 --- a/lib/screens/syncing/widgets/synced_season_poster.dart +++ b/lib/screens/syncing/widgets/synced_season_poster.dart @@ -1,13 +1,17 @@ +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync_provider.dart'; -import 'package:fladder/screens/shared/animated_fade_size.dart'; +import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/syncing/widgets/synced_episode_item.dart'; import 'package:fladder/util/fladder_image.dart'; -import 'package:fladder/util/list_padding.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:fladder/widgets/shared/icon_button_await.dart'; class SyncedSeasonPoster extends ConsumerStatefulWidget { const SyncedSeasonPoster({ @@ -29,14 +33,21 @@ class _SyncedSeasonPosterState extends ConsumerState { Widget build(BuildContext context) { final season = widget.season; final children = ref.read(syncProvider.notifier).getChildren(widget.syncedItem); - return Column( - children: [ - Row( - children: [ - SizedBox( - width: 125, - child: AspectRatio( - aspectRatio: 0.65, + final unSyncedChildren = children.where((child) => child.status == SyncStatus.partially).toList(); + return ExpansionTile( + tilePadding: EdgeInsets.zero, + title: Row( + spacing: 6, + children: [ + SizedBox( + width: 75, + child: AspectRatio( + aspectRatio: 0.65, + child: FlatButton( + onTap: () { + season.navigateTo(context); + return context.maybePop(); + }, child: Card( child: FladderImage( image: season.getPosters?.primary ?? @@ -46,50 +57,47 @@ class _SyncedSeasonPosterState extends ConsumerState { ), ), ), - Column( - children: [ - Text( - season.name, - style: Theme.of(context).textTheme.titleMedium, - ) - ], - ), - const Spacer(), - IconButton( - onPressed: () { - setState(() { - expanded = !expanded; - }); + ), + Column( + children: [ + Text( + season.name, + style: Theme.of(context).textTheme.titleMedium, + ) + ], + ), + ], + ), + trailing: Row( + mainAxisSize: MainAxisSize.min, + children: [ + if (unSyncedChildren.isNotEmpty) + IconButtonAwait( + onPressed: () async { + for (var i = 0; i < unSyncedChildren.length; i++) { + final childSyncedItem = unSyncedChildren[i]; + await ref.read(syncProvider.notifier).syncFile(childSyncedItem, false); + } }, - icon: Icon(!expanded ? Icons.keyboard_arrow_down_rounded : Icons.keyboard_arrow_up_rounded), - ) - ].addPadding(const EdgeInsets.symmetric(horizontal: 6)), - ), - AnimatedFadeSize( - duration: const Duration(milliseconds: 250), - child: expanded && children.isNotEmpty - ? ListView( - shrinkWrap: true, - physics: const NeverScrollableScrollPhysics(), - children: [ - const Divider(), - ...children.map( - (item) { - final baseItem = ref.read(syncProvider.notifier).getItem(item); - return IntrinsicHeight( - child: SyncedEpisodeItem( - episode: baseItem as EpisodeModel, - syncedItem: item, - hasFile: item.videoFile.existsSync(), - ), - ); - }, - ) - ].addPadding(const EdgeInsets.symmetric(vertical: 10)), - ) - : Container(), - ) - ].addPadding(EdgeInsets.only(top: 10, bottom: expanded ? 10 : 0)), + icon: const Icon(IconsaxPlusLinear.cloud_change), + ), + ], + ), + children: children.map( + (item) { + final baseItem = ref.read(syncProvider.notifier).getItem(item); + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: IntrinsicHeight( + child: SyncedEpisodeItem( + episode: baseItem as EpisodeModel, + syncedItem: item, + hasFile: item.videoFile.existsSync(), + ), + ), + ); + }, + ).toList(), ); } } diff --git a/lib/theme.dart b/lib/theme.dart index 620e283..958f653 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -126,9 +126,10 @@ class FladderTheme { listTileTheme: ListTileThemeData( shape: defaultShape, ), - dividerTheme: const DividerThemeData( + dividerTheme: DividerThemeData( indent: 6, endIndent: 6, + color: scheme?.onSurface.withAlpha(125), ), segmentedButtonTheme: SegmentedButtonThemeData( style: ButtonStyle( diff --git a/lib/util/item_base_model/item_base_model_extensions.dart b/lib/util/item_base_model/item_base_model_extensions.dart index dcdca9c..d002c28 100644 --- a/lib/util/item_base_model/item_base_model_extensions.dart +++ b/lib/util/item_base_model/item_base_model_extensions.dart @@ -9,6 +9,7 @@ import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/photos_model.dart'; +import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/collections/add_to_collection.dart'; @@ -243,8 +244,12 @@ extension ItemBaseModelExtensions on ItemBaseModel { else ItemActionButton( icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)), - action: () => showSyncItemDetails(context, syncedItem, ref), - label: Text(context.localized.syncDetails), + action: () => syncedItem.status == SyncStatus.complete + ? ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, null) + : ref.read(syncProvider.notifier).syncFile(syncedItem, false), + label: Text( + syncedItem.status == SyncStatus.complete ? context.localized.delete : context.localized.sync, + ), ) else if (downloadUrl != null) ...[ ItemActionButton( diff --git a/lib/util/localization_helper.dart b/lib/util/localization_helper.dart index 2d2dc6e..e6ce622 100644 --- a/lib/util/localization_helper.dart +++ b/lib/util/localization_helper.dart @@ -3,6 +3,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:fladder/l10n/generated/app_localizations.dart'; +import 'package:fladder/providers/sync/background_download_provider.dart'; ///Only use for base translations, under normal circumstances ALWAYS use the widgets provided context final localizationContextProvider = StateProvider((ref) => null); @@ -13,7 +14,12 @@ extension BuildContextExtension on BuildContext { class LocalizationContextWrapper extends ConsumerStatefulWidget { final Widget child; - const LocalizationContextWrapper({required this.child, super.key}); + final Locale currentLocale; + const LocalizationContextWrapper({ + required this.child, + required this.currentLocale, + super.key, + }); @override ConsumerState createState() => _LocalizationContextWrapperState(); @@ -23,8 +29,21 @@ class _LocalizationContextWrapperState extends ConsumerState context); + ref.read(backgroundDownloaderProvider.notifier).updateTranslations(context); }); } diff --git a/lib/widgets/navigation_scaffold/components/background_image.dart b/lib/widgets/navigation_scaffold/components/background_image.dart index e07fbf7..3defeda 100644 --- a/lib/widgets/navigation_scaffold/components/background_image.dart +++ b/lib/widgets/navigation_scaffold/components/background_image.dart @@ -59,8 +59,9 @@ class _BackgroundImageState extends ConsumerState { if (itemId == null) return; final apiResponse = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet(itemId: itemId); - final image = - apiResponse.body?.parentBaseModel.getPosters?.randomBackDrop ?? apiResponse.body?.getPosters?.randomBackDrop; + final image = apiResponse.body?.parentBaseModel.getPosters?.randomBackDrop ?? + apiResponse.body?.getPosters?.randomBackDrop ?? + apiResponse.body?.getPosters?.primary; if (mounted) setState(() => backgroundImage = image); }); diff --git a/lib/widgets/shared/icon_button_await.dart b/lib/widgets/shared/icon_button_await.dart index 2aaaa95..8e01e61 100644 --- a/lib/widgets/shared/icon_button_await.dart +++ b/lib/widgets/shared/icon_button_await.dart @@ -1,9 +1,10 @@ import 'dart:async'; import 'dart:developer'; -import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:flutter/material.dart'; +import 'package:fladder/screens/shared/animated_fade_size.dart'; + class IconButtonAwait extends StatefulWidget { final FutureOr Function() onPressed; final Color? color; @@ -33,7 +34,10 @@ class IconButtonAwaitState extends State { } catch (e) { log(e.toString()); } finally { - setState(() => loading = false); + setState(() { + if (!mounted) return; + loading = false; + }); } }, icon: AnimatedFadeSize( diff --git a/pubspec.lock b/pubspec.lock index 917cbff..e8d763e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -514,10 +514,10 @@ packages: dependency: "direct main" description: name: file_picker - sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" + sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a url: "https://pub.dev" source: hosted - version: "10.1.9" + version: "10.2.0" fixnum: dependency: transitive description: @@ -942,18 +942,18 @@ packages: dependency: "direct main" description: name: isar - sha256: ebf74d87c400bd9f7da14acb31932b50c2407edbbd40930da3a6c2a8143f85a8 - url: "https://pub.dev" + sha256: e987032e5d007a03ba4415cbf4d47add17b57ac664db8705db90fbfeb6a16737 + url: "https://pub.isar-community.dev" source: hosted - version: "4.0.0-dev.14" + version: "4.0.3" isar_flutter_libs: dependency: "direct main" description: name: isar_flutter_libs - sha256: "04a3f4035e213ddb6e78d0132a7c80296a085c2088c2a761b4a42ee5add36983" - url: "https://pub.dev" + sha256: a6b86d8618fe2d7d0e2ac6aa7a7f21c0c8ae912ccbef94a45d9f6e1e519ef610 + url: "https://pub.isar-community.dev" source: hosted - version: "4.0.0-dev.14" + version: "4.0.3" js: dependency: transitive description: @@ -2027,10 +2027,10 @@ packages: dependency: transitive description: name: vector_graphics - sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" + sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6 url: "https://pub.dev" source: hosted - version: "1.1.18" + version: "1.1.19" vector_graphics_codec: dependency: transitive description: @@ -2139,10 +2139,10 @@ packages: dependency: transitive description: name: watcher - sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" + sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a" url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" weak_map: dependency: transitive description: @@ -2211,10 +2211,10 @@ packages: dependency: transitive description: name: win32 - sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" + sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03" url: "https://pub.dev" source: hosted - version: "5.13.0" + version: "5.14.0" window_manager: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 7ceb72c..047c2ec 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -98,7 +98,7 @@ dependencies: # Utility path: ^1.9.1 - file_picker: ^10.1.9 + file_picker: ^10.2.0 transparent_image: ^2.0.1 universal_html: ^2.2.4 collection: ^1.19.1 @@ -114,8 +114,12 @@ dependencies: screen_retriever: ^0.2.0 # Data - isar: ^4.0.0-dev.14 - isar_flutter_libs: ^4.0.0-dev.14 # contains Isar Core + isar: + version: ^4.0.3 + hosted: https://pub.isar-community.dev/ + isar_flutter_libs: # contains Isar Core + version: ^4.0.3 + hosted: https://pub.isar-community.dev/ # Other async: ^2.13.0