diff --git a/.vscode/launch.json b/.vscode/launch.json index db1705f..ee28073 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -81,7 +81,6 @@ "args": [ "--web-port", "9090", - "--web-experimental-hot-reload" ], }, { @@ -93,7 +92,6 @@ "args": [ "--web-port", "9090", - "--web-experimental-hot-reload" ], }, ], diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 0232257..bdb9329 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1238,5 +1238,56 @@ }, "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", + "syncAllItemsTitle": "Sync all items from {itemName}?", + "@syncAllItemsTitle": { + "description": "syncAllItemsFrom", + "placeholders": { + "itemName": { + "type": "String" + } + } + }, + "syncAllItemsDesc": "This will sync ({itemCount}) items from '{itemName}' to your device.\nThis can take a while depending on the amount of items.", + "@syncAllItemsDesc": { + "description": "syncAllitemsFromDesc", + "placeholders": { + "itemName": { + "type": "String" + }, + "itemCount": { + "type": "int" + } + } + }, + "syncDeleteAllItemsTitle": "Delete all synced items from {itemName}?", + "@syncDeleteAllItemsTitle": { + "description": "syncDeleteAllitemsFrom", + "placeholders": { + "itemName": { + "type": "String" + } + } + }, + "syncDeleteAllItemsDesc": "This will delete all synced items from '{itemName}'.\nThis is permanent and you will need to re-sync ({itemCount}) files.", + "@syncDeleteAllItemsDesc": { + "description": "syncDeleteAllitemsFromDesc", + "placeholders": { + "itemName": { + "type": "String" + }, + "itemCount": { + "type": "int" + } + } + }, + "syncPauseAll": "Pause all", + "syncResumeAll": "Resume all", + "syncStopAll": "Stop all", + "syncDeleteAll": "Delete all files", + "syncAllFiles": "Sync all files" } \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index e21ea46..a48b71f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -8,9 +8,7 @@ import 'package:flutter/services.dart'; import 'package:dynamic_color/dynamic_color.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:isar/isar.dart'; import 'package:package_info_plus/package_info_plus.dart'; -import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; import 'package:shared_preferences/shared_preferences.dart'; import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:fladder/stubs/web/smtc_web.dart'; @@ -20,7 +18,6 @@ import 'package:window_manager/window_manager.dart'; import 'package:fladder/l10n/generated/app_localizations.dart'; import 'package:fladder/models/account_model.dart'; import 'package:fladder/models/settings/arguments_model.dart'; -import 'package:fladder/models/syncing/i_synced_item.dart'; import 'package:fladder/providers/arguments_provider.dart'; import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart'; @@ -71,13 +68,10 @@ void main(List args) async { PackageInfo packageInfo = await PackageInfo.fromPlatform(); - Directory isarPath = Directory(""); Directory applicationDirectory = Directory(""); if (!kIsWeb) { applicationDirectory = await getApplicationDocumentsDirectory(); - isarPath = Directory(path.joinAll([applicationDirectory.path, 'Fladder', 'Database'])); - await isarPath.create(recursive: true); } if (_isDesktop) { @@ -98,16 +92,7 @@ void main(List args) async { applicationInfoProvider.overrideWith((ref) => applicationInfo), crashLogProvider.overrideWith((ref) => crashProvider), argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args)), - syncProvider.overrideWith((ref) => SyncNotifier( - ref, - !kIsWeb - ? Isar.open( - schemas: [ISyncedItemSchema], - directory: isarPath.path, - ) - : null, - applicationDirectory, - )) + syncProvider.overrideWith((ref) => SyncNotifier(ref, applicationDirectory)) ], child: AdaptiveLayoutBuilder( child: (context) => const Main(), @@ -297,6 +282,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/episode_model.dart b/lib/models/items/episode_model.dart index cb6d7b0..4ed4351 100644 --- a/lib/models/items/episode_model.dart +++ b/lib/models/items/episode_model.dart @@ -45,6 +45,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable { final String? seriesName; final int season; final int episode; + final int? episodeEnd; final List chapters; final ItemLocation? location; final DateTime? dateAired; @@ -52,6 +53,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable { required this.seriesName, required this.season, required this.episode, + required this.episodeEnd, this.chapters = const [], this.location, this.dateAired, @@ -134,12 +136,26 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable { String seasonAnnotation(BuildContext context) => context.localized.season(1)[0]; String episodeAnnotation(BuildContext context) => context.localized.episode(1)[0]; + int get episodeCount { + if (episodeEnd != null && episodeEnd! > episode) { + return episodeEnd! - episode + 1; + } + return 1; + } + + String get episodeRange { + if (episodeEnd != null && episodeEnd! > episode) { + return "$episode-${episodeEnd!}"; + } + return episode.toString(); + } + String seasonEpisodeLabel(BuildContext context) { - return "${seasonAnnotation(context)}$season - ${episodeAnnotation(context)}$episode"; + return "${seasonAnnotation(context)}$season - ${episodeAnnotation(context)}$episodeRange"; } String seasonEpisodeLabelFull(BuildContext context) { - return "${context.localized.season(1)} $season - ${context.localized.episode(1)} $episode"; + return "${context.localized.season(1)} $season - ${context.localized.episode(episodeCount)} $episodeRange"; } String episodeLabel(BuildContext context) { @@ -147,7 +163,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable { } String get fullName { - return "$episode. $subText"; + return "$episodeRange. $subText"; } @override @@ -169,6 +185,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable { primaryRatio: item.primaryImageAspectRatio, season: item.parentIndexNumber ?? 0, episode: item.indexNumber ?? 0, + episodeEnd: item.indexNumberEnd, location: ItemLocation.fromDto(item.locationType), parentImages: ImagesData.fromBaseItemParent(item, ref), canDelete: item.canDelete, diff --git a/lib/models/items/episode_model.mapper.dart b/lib/models/items/episode_model.mapper.dart index f5714d1..0b0bb48 100644 --- a/lib/models/items/episode_model.mapper.dart +++ b/lib/models/items/episode_model.mapper.dart @@ -31,6 +31,9 @@ class EpisodeModelMapper extends SubClassMapperBase { static int _$episode(EpisodeModel v) => v.episode; static const Field _f$episode = Field('episode', _$episode); + static int? _$episodeEnd(EpisodeModel v) => v.episodeEnd; + static const Field _f$episodeEnd = + Field('episodeEnd', _$episodeEnd); static List _$chapters(EpisodeModel v) => v.chapters; static const Field> _f$chapters = Field('chapters', _$chapters, opt: true, def: const []); @@ -86,6 +89,7 @@ class EpisodeModelMapper extends SubClassMapperBase { #seriesName: _f$seriesName, #season: _f$season, #episode: _f$episode, + #episodeEnd: _f$episodeEnd, #chapters: _f$chapters, #location: _f$location, #dateAired: _f$dateAired, @@ -120,6 +124,7 @@ class EpisodeModelMapper extends SubClassMapperBase { seriesName: data.dec(_f$seriesName), season: data.dec(_f$season), episode: data.dec(_f$episode), + episodeEnd: data.dec(_f$episodeEnd), chapters: data.dec(_f$chapters), location: data.dec(_f$location), dateAired: data.dec(_f$dateAired), @@ -166,6 +171,7 @@ abstract class EpisodeModelCopyWith<$R, $In extends EpisodeModel, $Out> {String? seriesName, int? season, int? episode, + int? episodeEnd, List? chapters, ItemLocation? location, DateTime? dateAired, @@ -209,6 +215,7 @@ class _EpisodeModelCopyWithImpl<$R, $Out> {Object? seriesName = $none, int? season, int? episode, + Object? episodeEnd = $none, List? chapters, Object? location = $none, Object? dateAired = $none, @@ -230,6 +237,7 @@ class _EpisodeModelCopyWithImpl<$R, $Out> if (seriesName != $none) #seriesName: seriesName, if (season != null) #season: season, if (episode != null) #episode: episode, + if (episodeEnd != $none) #episodeEnd: episodeEnd, if (chapters != null) #chapters: chapters, if (location != $none) #location: location, if (dateAired != $none) #dateAired: dateAired, @@ -253,6 +261,7 @@ class _EpisodeModelCopyWithImpl<$R, $Out> seriesName: data.get(#seriesName, or: $value.seriesName), season: data.get(#season, or: $value.season), episode: data.get(#episode, or: $value.episode), + episodeEnd: data.get(#episodeEnd, or: $value.episodeEnd), chapters: data.get(#chapters, or: $value.chapters), location: data.get(#location, or: $value.location), dateAired: data.get(#dateAired, or: $value.dateAired), diff --git a/lib/models/items/item_shared_models.dart b/lib/models/items/item_shared_models.dart index f20acc6..370fc7d 100644 --- a/lib/models/items/item_shared_models.dart +++ b/lib/models/items/item_shared_models.dart @@ -50,6 +50,21 @@ class UserData with UserDataMappable { Duration get playBackPosition => Duration(milliseconds: playbackPositionTicks ~/ 10000); + static UserData? determineLastUserData(List data) { + return data.where((data) => data != null).reduce((a, b) { + final aDate = a?.lastPlayed; + final bDate = b?.lastPlayed; + + if (aDate != null && bDate != null) { + return aDate.isAfter(bDate) ? a : b; + } else if (aDate != null) { + return a; + } else { + return b; + } + }); + } + factory UserData.fromMap(Map map) => UserDataMapper.fromMap(map); factory UserData.fromJson(String json) => UserDataMapper.fromJson(json); } diff --git a/lib/models/items/season_model.dart b/lib/models/items/season_model.dart index 8101826..9e43fbe 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 => episodes.isNotEmpty && episodes.any((element) => element.syncAble); + @override ImagesData? get getPosters => images ?? parentImages; diff --git a/lib/models/playback/playback_model.dart b/lib/models/playback/playback_model.dart index befe0f9..afac46c 100644 --- a/lib/models/playback/playback_model.dart +++ b/lib/models/playback/playback_model.dart @@ -2,6 +2,7 @@ import 'dart:developer'; import 'package:flutter/material.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:chopper/chopper.dart'; import 'package:collection/collection.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; @@ -26,7 +27,6 @@ import 'package:fladder/profiles/default_profile.dart'; import 'package:fladder/providers/api_provider.dart'; import 'package:fladder/providers/service_provider.dart'; import 'package:fladder/providers/settings/video_player_settings_provider.dart'; -import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/video_player_provider.dart'; @@ -129,7 +129,7 @@ class PlaybackModelHelper { await _createOfflinePlaybackModel( newItem, null, - ref.read(syncProvider.notifier).getSyncedItem(newItem), + await ref.read(syncProvider.notifier).getSyncedItem(newItem), oldModel: currentModel, ); if (newModel == null) return null; @@ -143,10 +143,10 @@ class PlaybackModelHelper { SyncedItem? syncedItem, { PlaybackModel? oldModel, }) async { - final ItemBaseModel? syncedItemModel = ref.read(syncProvider.notifier).getItem(syncedItem); + final ItemBaseModel? syncedItemModel = syncedItem?.itemModel; if (syncedItemModel == null || syncedItem == null || !syncedItem.dataFile.existsSync()) return null; - final children = ref.read(syncChildrenProvider(syncedItem)); + final children = await ref.read(syncProvider.notifier).getChildren(syncedItem); final syncedItems = children.where((element) => element.videoFile.existsSync()).toList(); final itemQueue = syncedItems.map((e) => e.createItemModel(ref)); @@ -174,67 +174,81 @@ class PlaybackModelHelper { final userId = ref.read(userProvider)?.id; if (userId?.isEmpty == true) return null; - final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item); + try { + final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item); - final firstItemToPlay = switch (item) { - SeriesModel _ || SeasonModel _ => (queue.whereType().toList().nextUp), - _ => item, - }; - - if (firstItemToPlay == null) return null; - - final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body; - - if (fullItem == null) return null; - - SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(fullItem); - - final firstItemIsSynced = syncedItem != null && syncedItem.status == SyncStatus.complete; - - final options = { - PlaybackType.directStream, - PlaybackType.transcode, - if (firstItemIsSynced) PlaybackType.offline, - }; - - if ((showPlaybackOptions || firstItemIsSynced) && context != null) { - final playbackType = await showPlaybackTypeSelection( - context: context, - options: options, - ); - - if (!context.mounted) return null; - - return switch (playbackType) { - PlaybackType.directStream || PlaybackType.transcode => await _createServerPlaybackModel( - fullItem, - item.streamModel, - playbackType, - oldModel: oldModel, - libraryQueue: queue, - startPosition: startPosition, - ), - PlaybackType.offline => await _createOfflinePlaybackModel( - fullItem, - item.streamModel, - syncedItem, - ), - null => null + final firstItemToPlay = switch (item) { + SeriesModel _ || SeasonModel _ => (queue.whereType().toList().nextUp), + _ => item, }; - } else { - return (await _createServerPlaybackModel( - fullItem, - item.streamModel, - PlaybackType.directStream, - startPosition: startPosition, - oldModel: oldModel, - libraryQueue: queue, - )) ?? - await _createOfflinePlaybackModel( - fullItem, - item.streamModel, - syncedItem, - ); + + if (firstItemToPlay == null) return null; + + final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body; + + if (fullItem == null) return null; + + SyncedItem? syncedItem = await ref.read(syncProvider.notifier).getSyncedItem(fullItem); + + final firstItemIsSynced = syncedItem != null && syncedItem.status == TaskStatus.complete; + + final options = { + PlaybackType.directStream, + PlaybackType.transcode, + if (firstItemIsSynced) PlaybackType.offline, + }; + + if ((showPlaybackOptions || firstItemIsSynced) && context != null) { + final playbackType = await showPlaybackTypeSelection( + context: context, + options: options, + ); + + if (!context.mounted) return null; + + return switch (playbackType) { + PlaybackType.directStream || PlaybackType.transcode => await _createServerPlaybackModel( + fullItem, + item.streamModel, + playbackType, + oldModel: oldModel, + libraryQueue: queue, + startPosition: startPosition, + ), + PlaybackType.offline => await _createOfflinePlaybackModel( + fullItem, + item.streamModel, + syncedItem, + ), + null => null + }; + } else { + return (await _createServerPlaybackModel( + fullItem, + item.streamModel, + PlaybackType.directStream, + startPosition: startPosition, + oldModel: oldModel, + libraryQueue: queue, + )) ?? + await _createOfflinePlaybackModel( + fullItem, + item.streamModel, + syncedItem, + ); + } + } catch (e) { + log(e.toString()); + SyncedItem? syncedItem = await ref.read(syncProvider.notifier).getSyncedItem(item); + if (syncedItem != null) { + return await _createOfflinePlaybackModel( + item, + item.streamModel, + syncedItem, + oldModel: oldModel, + ); + } + return null; } } diff --git a/lib/models/syncing/database_item.dart b/lib/models/syncing/database_item.dart new file mode 100644 index 0000000..d7d3a70 --- /dev/null +++ b/lib/models/syncing/database_item.dart @@ -0,0 +1,193 @@ +import 'dart:convert'; + +import 'package:drift/drift.dart'; +import 'package:drift_flutter/drift_flutter.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:path_provider/path_provider.dart'; + +import 'package:fladder/models/item_base_model.dart'; +import 'package:fladder/models/items/chapters_model.dart'; +import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/models/items/item_shared_models.dart'; +import 'package:fladder/models/items/media_segments_model.dart'; +import 'package:fladder/models/items/media_streams_model.dart'; +import 'package:fladder/models/items/trick_play_model.dart'; +import 'package:fladder/models/syncing/sync_item.dart'; +import 'package:fladder/providers/user_provider.dart'; + +part 'database_item.g.dart'; + +@TableIndex(name: 'database_id', columns: {#id}) +class DatabaseItems extends Table { + TextColumn get userId => text()(); + TextColumn get id => text().withLength(min: 1)(); + BoolColumn get syncing => boolean()(); + TextColumn get sortName => text().nullable()(); + TextColumn get parentId => text().nullable()(); + TextColumn get path => text().nullable()(); + IntColumn get fileSize => integer().nullable()(); + TextColumn get videoFileName => text().nullable()(); + TextColumn get trickPlayModel => text().nullable()(); + TextColumn get mediaSegments => text().nullable()(); + TextColumn get images => text().nullable()(); + TextColumn get chapters => text().nullable()(); + TextColumn get subtitles => text().nullable()(); + TextColumn get userData => text().nullable()(); + + @override + Set> get primaryKey => {id, userId}; +} + +@DriftDatabase(tables: [DatabaseItems]) +class AppDatabase extends _$AppDatabase { + AppDatabase(this.ref, [QueryExecutor? executor]) : super(executor ?? _openConnection()); + + final Ref ref; + + String get userId => ref.read(userProvider.select((value) => value?.id ?? "")); + + @override + int get schemaVersion => 1; + + Future clearDatabase() { + return transaction(() async { + for (final table in allTables) { + await delete(table).go(); + } + }); + } + + Selectable getItem(String id) => + (select(databaseItems)..where((tbl) => tbl.id.equals(id) & tbl.userId.equals(userId))).map(databaseConverter); + + Selectable getParent(String id) => + (select(databaseItems)..where((tbl) => tbl.parentId.equals(id) & tbl.userId.equals(userId))) + .map(databaseConverter); + + Selectable get getParentItems => + ((select(databaseItems)..where((tbl) => (tbl.parentId.isNull() & tbl.userId.equals(userId)))) + ..orderBy([(t) => OrderingTerm(expression: t.sortName)])) + .map(databaseConverter); + + Selectable getChildren(String parentId) => + ((select(databaseItems)..where((tbl) => (tbl.parentId.equals(parentId) & tbl.userId.equals(userId)))) + ..orderBy([(t) => OrderingTerm(expression: t.sortName)])) + .map(databaseConverter); + + Future insertItem(SyncedItem item) async { + final itemExists = await getItem(item.id).getSingleOrNull(); + if (itemExists != null) { + return (update(databaseItems)..where((tbl) => tbl.id.equals(item.id) & tbl.userId.equals(userId))) + .write(toDataBaseItem(item)); + } else { + return into(databaseItems).insert(toDataBaseItem(item)); + } + } + + Future> getNestedChildren(SyncedItem root) async { + final itemType = root.createItemModel(ref)?.type; + + if (itemType == null) return []; + + final int maxDepth = switch (itemType) { + FladderItemType.episode => 0, + FladderItemType.movie => 0, + FladderItemType.season => 1, + FladderItemType.series => 2, + _ => 1, + }; + + final all = []; + List toProcess = [root]; + + if (maxDepth == 0) { + return []; + } + + for (var i = 0; i < maxDepth; i++) { + final futures = toProcess.map((item) => getChildren(item.id).get()); + final resultsList = await Future.wait(futures); + + final children = resultsList.expand((r) => r).toList(); + + if (children.isEmpty) break; + + all.addAll(children); + toProcess = children; + } + + return all; + } + + Future insertMultipleEntries(List items) async { + await batch((batch) { + batch.insertAll( + databaseItems, + items.map(toDataBaseItem), + mode: InsertMode.insertOrReplace, + ); + }); + } + + Future deleteAllItems(List items) async => await batch((batch) { + batch.deleteWhere(databaseItems, (tbl) => tbl.id.isIn(items.map((e) => e.id))); + }); + + DatabaseItemsCompanion toDataBaseItem(SyncedItem item) { + return DatabaseItemsCompanion( + id: Value(item.id), + parentId: Value(item.parentId), + syncing: Value(item.syncing), + userId: Value(userId), + path: Value(item.path), + fileSize: Value(item.fileSize), + sortName: Value(item.sortName), + videoFileName: Value(item.videoFileName), + trickPlayModel: Value(item.fTrickPlayModel != null ? jsonEncode(item.fTrickPlayModel?.toJson()) : null), + mediaSegments: Value(item.mediaSegments != null ? jsonEncode(item.mediaSegments?.toJson()) : null), + images: Value(item.fImages != null ? jsonEncode(item.fImages?.toJson()) : null), + chapters: Value(jsonEncode(item.fChapters.map((e) => e.toJson()).toList())), + subtitles: Value(jsonEncode(item.subtitles.map((e) => e.toJson()).toList())), + userData: Value(item.userData != null ? jsonEncode(item.userData?.toJson()) : null), + ); + } + + SyncedItem databaseConverter(DatabaseItem dataItem) { + final syncedItem = SyncedItem( + id: dataItem.id, + userId: dataItem.userId, + parentId: dataItem.parentId, + sortName: dataItem.sortName, + syncing: dataItem.syncing, + path: dataItem.path, + fileSize: dataItem.fileSize, + videoFileName: dataItem.videoFileName, + fTrickPlayModel: + dataItem.trickPlayModel != null ? TrickPlayModel.fromJson(jsonDecode(dataItem.trickPlayModel!)) : null, + mediaSegments: + dataItem.mediaSegments != null ? MediaSegmentsModel.fromJson(jsonDecode(dataItem.mediaSegments!)) : null, + fImages: dataItem.images != null ? ImagesData.fromJson(jsonDecode(dataItem.images!)) : null, + fChapters: (dataItem.chapters != null && dataItem.chapters!.isNotEmpty) + ? (jsonDecode(dataItem.chapters!) as List).map((e) => Chapter.fromJson(e)).toList() + : [], + subtitles: (dataItem.subtitles != null && dataItem.subtitles!.isNotEmpty) + ? (jsonDecode(dataItem.subtitles!) as List).map((e) => SubStreamModel.fromJson(e)).toList() + : [], + userData: dataItem.userData != null ? UserData.fromJson(jsonDecode(dataItem.userData!)) : null, + ); + + return syncedItem.copyWith( + itemModel: syncedItem.createItemModel(ref), + ); + } + + static QueryExecutor _openConnection() { + return driftDatabase( + name: 'syncedDatabase', + native: const DriftNativeOptions( + databaseDirectory: getApplicationSupportDirectory, + ), + // If you need web support, see https://drift.simonbinder.eu/platforms/web/ + ); + } +} diff --git a/lib/models/syncing/database_item.g.dart b/lib/models/syncing/database_item.g.dart new file mode 100644 index 0000000..96fa85e --- /dev/null +++ b/lib/models/syncing/database_item.g.dart @@ -0,0 +1,1035 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'database_item.dart'; + +// ignore_for_file: type=lint +class $DatabaseItemsTable extends DatabaseItems + with TableInfo<$DatabaseItemsTable, DatabaseItem> { + @override + final GeneratedDatabase attachedDatabase; + final String? _alias; + $DatabaseItemsTable(this.attachedDatabase, [this._alias]); + static const VerificationMeta _userIdMeta = const VerificationMeta('userId'); + @override + late final GeneratedColumn userId = GeneratedColumn( + 'user_id', aliasedName, false, + type: DriftSqlType.string, requiredDuringInsert: true); + static const VerificationMeta _idMeta = const VerificationMeta('id'); + @override + late final GeneratedColumn id = + GeneratedColumn('id', aliasedName, false, + additionalChecks: GeneratedColumn.checkTextLength( + minTextLength: 1, + ), + type: DriftSqlType.string, + requiredDuringInsert: true); + static const VerificationMeta _syncingMeta = + const VerificationMeta('syncing'); + @override + late final GeneratedColumn syncing = GeneratedColumn( + 'syncing', aliasedName, false, + type: DriftSqlType.bool, + requiredDuringInsert: true, + defaultConstraints: + GeneratedColumn.constraintIsAlways('CHECK ("syncing" IN (0, 1))')); + static const VerificationMeta _sortNameMeta = + const VerificationMeta('sortName'); + @override + late final GeneratedColumn sortName = GeneratedColumn( + 'sort_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _parentIdMeta = + const VerificationMeta('parentId'); + @override + late final GeneratedColumn parentId = GeneratedColumn( + 'parent_id', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _pathMeta = const VerificationMeta('path'); + @override + late final GeneratedColumn path = GeneratedColumn( + 'path', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _fileSizeMeta = + const VerificationMeta('fileSize'); + @override + late final GeneratedColumn fileSize = GeneratedColumn( + 'file_size', aliasedName, true, + type: DriftSqlType.int, requiredDuringInsert: false); + static const VerificationMeta _videoFileNameMeta = + const VerificationMeta('videoFileName'); + @override + late final GeneratedColumn videoFileName = GeneratedColumn( + 'video_file_name', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _trickPlayModelMeta = + const VerificationMeta('trickPlayModel'); + @override + late final GeneratedColumn trickPlayModel = GeneratedColumn( + 'trick_play_model', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _mediaSegmentsMeta = + const VerificationMeta('mediaSegments'); + @override + late final GeneratedColumn mediaSegments = GeneratedColumn( + 'media_segments', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _imagesMeta = const VerificationMeta('images'); + @override + late final GeneratedColumn images = GeneratedColumn( + 'images', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _chaptersMeta = + const VerificationMeta('chapters'); + @override + late final GeneratedColumn chapters = GeneratedColumn( + 'chapters', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _subtitlesMeta = + const VerificationMeta('subtitles'); + @override + late final GeneratedColumn subtitles = GeneratedColumn( + 'subtitles', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + static const VerificationMeta _userDataMeta = + const VerificationMeta('userData'); + @override + late final GeneratedColumn userData = GeneratedColumn( + 'user_data', aliasedName, true, + type: DriftSqlType.string, requiredDuringInsert: false); + @override + List get $columns => [ + userId, + id, + syncing, + sortName, + parentId, + path, + fileSize, + videoFileName, + trickPlayModel, + mediaSegments, + images, + chapters, + subtitles, + userData + ]; + @override + String get aliasedName => _alias ?? actualTableName; + @override + String get actualTableName => $name; + static const String $name = 'database_items'; + @override + VerificationContext validateIntegrity(Insertable instance, + {bool isInserting = false}) { + final context = VerificationContext(); + final data = instance.toColumns(true); + if (data.containsKey('user_id')) { + context.handle(_userIdMeta, + userId.isAcceptableOrUnknown(data['user_id']!, _userIdMeta)); + } else if (isInserting) { + context.missing(_userIdMeta); + } + if (data.containsKey('id')) { + context.handle(_idMeta, id.isAcceptableOrUnknown(data['id']!, _idMeta)); + } else if (isInserting) { + context.missing(_idMeta); + } + if (data.containsKey('syncing')) { + context.handle(_syncingMeta, + syncing.isAcceptableOrUnknown(data['syncing']!, _syncingMeta)); + } else if (isInserting) { + context.missing(_syncingMeta); + } + if (data.containsKey('sort_name')) { + context.handle(_sortNameMeta, + sortName.isAcceptableOrUnknown(data['sort_name']!, _sortNameMeta)); + } + if (data.containsKey('parent_id')) { + context.handle(_parentIdMeta, + parentId.isAcceptableOrUnknown(data['parent_id']!, _parentIdMeta)); + } + if (data.containsKey('path')) { + context.handle( + _pathMeta, path.isAcceptableOrUnknown(data['path']!, _pathMeta)); + } + if (data.containsKey('file_size')) { + context.handle(_fileSizeMeta, + fileSize.isAcceptableOrUnknown(data['file_size']!, _fileSizeMeta)); + } + if (data.containsKey('video_file_name')) { + context.handle( + _videoFileNameMeta, + videoFileName.isAcceptableOrUnknown( + data['video_file_name']!, _videoFileNameMeta)); + } + if (data.containsKey('trick_play_model')) { + context.handle( + _trickPlayModelMeta, + trickPlayModel.isAcceptableOrUnknown( + data['trick_play_model']!, _trickPlayModelMeta)); + } + if (data.containsKey('media_segments')) { + context.handle( + _mediaSegmentsMeta, + mediaSegments.isAcceptableOrUnknown( + data['media_segments']!, _mediaSegmentsMeta)); + } + if (data.containsKey('images')) { + context.handle(_imagesMeta, + images.isAcceptableOrUnknown(data['images']!, _imagesMeta)); + } + if (data.containsKey('chapters')) { + context.handle(_chaptersMeta, + chapters.isAcceptableOrUnknown(data['chapters']!, _chaptersMeta)); + } + if (data.containsKey('subtitles')) { + context.handle(_subtitlesMeta, + subtitles.isAcceptableOrUnknown(data['subtitles']!, _subtitlesMeta)); + } + if (data.containsKey('user_data')) { + context.handle(_userDataMeta, + userData.isAcceptableOrUnknown(data['user_data']!, _userDataMeta)); + } + return context; + } + + @override + Set get $primaryKey => {id, userId}; + @override + DatabaseItem map(Map data, {String? tablePrefix}) { + final effectivePrefix = tablePrefix != null ? '$tablePrefix.' : ''; + return DatabaseItem( + userId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_id'])!, + id: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}id'])!, + syncing: attachedDatabase.typeMapping + .read(DriftSqlType.bool, data['${effectivePrefix}syncing'])!, + sortName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}sort_name']), + parentId: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}parent_id']), + path: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}path']), + fileSize: attachedDatabase.typeMapping + .read(DriftSqlType.int, data['${effectivePrefix}file_size']), + videoFileName: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}video_file_name']), + trickPlayModel: attachedDatabase.typeMapping.read( + DriftSqlType.string, data['${effectivePrefix}trick_play_model']), + mediaSegments: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}media_segments']), + images: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}images']), + chapters: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}chapters']), + subtitles: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}subtitles']), + userData: attachedDatabase.typeMapping + .read(DriftSqlType.string, data['${effectivePrefix}user_data']), + ); + } + + @override + $DatabaseItemsTable createAlias(String alias) { + return $DatabaseItemsTable(attachedDatabase, alias); + } +} + +class DatabaseItem extends DataClass implements Insertable { + final String userId; + final String id; + final bool syncing; + final String? sortName; + final String? parentId; + final String? path; + final int? fileSize; + final String? videoFileName; + final String? trickPlayModel; + final String? mediaSegments; + final String? images; + final String? chapters; + final String? subtitles; + final String? userData; + const DatabaseItem( + {required this.userId, + required this.id, + required this.syncing, + this.sortName, + this.parentId, + this.path, + this.fileSize, + this.videoFileName, + this.trickPlayModel, + this.mediaSegments, + this.images, + this.chapters, + this.subtitles, + this.userData}); + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + map['user_id'] = Variable(userId); + map['id'] = Variable(id); + map['syncing'] = Variable(syncing); + if (!nullToAbsent || sortName != null) { + map['sort_name'] = Variable(sortName); + } + if (!nullToAbsent || parentId != null) { + map['parent_id'] = Variable(parentId); + } + if (!nullToAbsent || path != null) { + map['path'] = Variable(path); + } + if (!nullToAbsent || fileSize != null) { + map['file_size'] = Variable(fileSize); + } + if (!nullToAbsent || videoFileName != null) { + map['video_file_name'] = Variable(videoFileName); + } + if (!nullToAbsent || trickPlayModel != null) { + map['trick_play_model'] = Variable(trickPlayModel); + } + if (!nullToAbsent || mediaSegments != null) { + map['media_segments'] = Variable(mediaSegments); + } + if (!nullToAbsent || images != null) { + map['images'] = Variable(images); + } + if (!nullToAbsent || chapters != null) { + map['chapters'] = Variable(chapters); + } + if (!nullToAbsent || subtitles != null) { + map['subtitles'] = Variable(subtitles); + } + if (!nullToAbsent || userData != null) { + map['user_data'] = Variable(userData); + } + return map; + } + + DatabaseItemsCompanion toCompanion(bool nullToAbsent) { + return DatabaseItemsCompanion( + userId: Value(userId), + id: Value(id), + syncing: Value(syncing), + sortName: sortName == null && nullToAbsent + ? const Value.absent() + : Value(sortName), + parentId: parentId == null && nullToAbsent + ? const Value.absent() + : Value(parentId), + path: path == null && nullToAbsent ? const Value.absent() : Value(path), + fileSize: fileSize == null && nullToAbsent + ? const Value.absent() + : Value(fileSize), + videoFileName: videoFileName == null && nullToAbsent + ? const Value.absent() + : Value(videoFileName), + trickPlayModel: trickPlayModel == null && nullToAbsent + ? const Value.absent() + : Value(trickPlayModel), + mediaSegments: mediaSegments == null && nullToAbsent + ? const Value.absent() + : Value(mediaSegments), + images: + images == null && nullToAbsent ? const Value.absent() : Value(images), + chapters: chapters == null && nullToAbsent + ? const Value.absent() + : Value(chapters), + subtitles: subtitles == null && nullToAbsent + ? const Value.absent() + : Value(subtitles), + userData: userData == null && nullToAbsent + ? const Value.absent() + : Value(userData), + ); + } + + factory DatabaseItem.fromJson(Map json, + {ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return DatabaseItem( + userId: serializer.fromJson(json['userId']), + id: serializer.fromJson(json['id']), + syncing: serializer.fromJson(json['syncing']), + sortName: serializer.fromJson(json['sortName']), + parentId: serializer.fromJson(json['parentId']), + path: serializer.fromJson(json['path']), + fileSize: serializer.fromJson(json['fileSize']), + videoFileName: serializer.fromJson(json['videoFileName']), + trickPlayModel: serializer.fromJson(json['trickPlayModel']), + mediaSegments: serializer.fromJson(json['mediaSegments']), + images: serializer.fromJson(json['images']), + chapters: serializer.fromJson(json['chapters']), + subtitles: serializer.fromJson(json['subtitles']), + userData: serializer.fromJson(json['userData']), + ); + } + @override + Map toJson({ValueSerializer? serializer}) { + serializer ??= driftRuntimeOptions.defaultSerializer; + return { + 'userId': serializer.toJson(userId), + 'id': serializer.toJson(id), + 'syncing': serializer.toJson(syncing), + 'sortName': serializer.toJson(sortName), + 'parentId': serializer.toJson(parentId), + 'path': serializer.toJson(path), + 'fileSize': serializer.toJson(fileSize), + 'videoFileName': serializer.toJson(videoFileName), + 'trickPlayModel': serializer.toJson(trickPlayModel), + 'mediaSegments': serializer.toJson(mediaSegments), + 'images': serializer.toJson(images), + 'chapters': serializer.toJson(chapters), + 'subtitles': serializer.toJson(subtitles), + 'userData': serializer.toJson(userData), + }; + } + + DatabaseItem copyWith( + {String? userId, + String? id, + bool? syncing, + Value sortName = const Value.absent(), + Value parentId = const Value.absent(), + Value path = const Value.absent(), + Value fileSize = const Value.absent(), + Value videoFileName = const Value.absent(), + Value trickPlayModel = const Value.absent(), + Value mediaSegments = const Value.absent(), + Value images = const Value.absent(), + Value chapters = const Value.absent(), + Value subtitles = const Value.absent(), + Value userData = const Value.absent()}) => + DatabaseItem( + userId: userId ?? this.userId, + id: id ?? this.id, + syncing: syncing ?? this.syncing, + sortName: sortName.present ? sortName.value : this.sortName, + parentId: parentId.present ? parentId.value : this.parentId, + path: path.present ? path.value : this.path, + fileSize: fileSize.present ? fileSize.value : this.fileSize, + videoFileName: + videoFileName.present ? videoFileName.value : this.videoFileName, + trickPlayModel: + trickPlayModel.present ? trickPlayModel.value : this.trickPlayModel, + mediaSegments: + mediaSegments.present ? mediaSegments.value : this.mediaSegments, + images: images.present ? images.value : this.images, + chapters: chapters.present ? chapters.value : this.chapters, + subtitles: subtitles.present ? subtitles.value : this.subtitles, + userData: userData.present ? userData.value : this.userData, + ); + DatabaseItem copyWithCompanion(DatabaseItemsCompanion data) { + return DatabaseItem( + userId: data.userId.present ? data.userId.value : this.userId, + id: data.id.present ? data.id.value : this.id, + syncing: data.syncing.present ? data.syncing.value : this.syncing, + sortName: data.sortName.present ? data.sortName.value : this.sortName, + parentId: data.parentId.present ? data.parentId.value : this.parentId, + path: data.path.present ? data.path.value : this.path, + fileSize: data.fileSize.present ? data.fileSize.value : this.fileSize, + videoFileName: data.videoFileName.present + ? data.videoFileName.value + : this.videoFileName, + trickPlayModel: data.trickPlayModel.present + ? data.trickPlayModel.value + : this.trickPlayModel, + mediaSegments: data.mediaSegments.present + ? data.mediaSegments.value + : this.mediaSegments, + images: data.images.present ? data.images.value : this.images, + chapters: data.chapters.present ? data.chapters.value : this.chapters, + subtitles: data.subtitles.present ? data.subtitles.value : this.subtitles, + userData: data.userData.present ? data.userData.value : this.userData, + ); + } + + @override + String toString() { + return (StringBuffer('DatabaseItem(') + ..write('userId: $userId, ') + ..write('id: $id, ') + ..write('syncing: $syncing, ') + ..write('sortName: $sortName, ') + ..write('parentId: $parentId, ') + ..write('path: $path, ') + ..write('fileSize: $fileSize, ') + ..write('videoFileName: $videoFileName, ') + ..write('trickPlayModel: $trickPlayModel, ') + ..write('mediaSegments: $mediaSegments, ') + ..write('images: $images, ') + ..write('chapters: $chapters, ') + ..write('subtitles: $subtitles, ') + ..write('userData: $userData') + ..write(')')) + .toString(); + } + + @override + int get hashCode => Object.hash( + userId, + id, + syncing, + sortName, + parentId, + path, + fileSize, + videoFileName, + trickPlayModel, + mediaSegments, + images, + chapters, + subtitles, + userData); + @override + bool operator ==(Object other) => + identical(this, other) || + (other is DatabaseItem && + other.userId == this.userId && + other.id == this.id && + other.syncing == this.syncing && + other.sortName == this.sortName && + other.parentId == this.parentId && + other.path == this.path && + other.fileSize == this.fileSize && + other.videoFileName == this.videoFileName && + other.trickPlayModel == this.trickPlayModel && + other.mediaSegments == this.mediaSegments && + other.images == this.images && + other.chapters == this.chapters && + other.subtitles == this.subtitles && + other.userData == this.userData); +} + +class DatabaseItemsCompanion extends UpdateCompanion { + final Value userId; + final Value id; + final Value syncing; + final Value sortName; + final Value parentId; + final Value path; + final Value fileSize; + final Value videoFileName; + final Value trickPlayModel; + final Value mediaSegments; + final Value images; + final Value chapters; + final Value subtitles; + final Value userData; + final Value rowid; + const DatabaseItemsCompanion({ + this.userId = const Value.absent(), + this.id = const Value.absent(), + this.syncing = const Value.absent(), + this.sortName = const Value.absent(), + this.parentId = const Value.absent(), + this.path = const Value.absent(), + this.fileSize = const Value.absent(), + this.videoFileName = const Value.absent(), + this.trickPlayModel = const Value.absent(), + this.mediaSegments = const Value.absent(), + this.images = const Value.absent(), + this.chapters = const Value.absent(), + this.subtitles = const Value.absent(), + this.userData = const Value.absent(), + this.rowid = const Value.absent(), + }); + DatabaseItemsCompanion.insert({ + required String userId, + required String id, + required bool syncing, + this.sortName = const Value.absent(), + this.parentId = const Value.absent(), + this.path = const Value.absent(), + this.fileSize = const Value.absent(), + this.videoFileName = const Value.absent(), + this.trickPlayModel = const Value.absent(), + this.mediaSegments = const Value.absent(), + this.images = const Value.absent(), + this.chapters = const Value.absent(), + this.subtitles = const Value.absent(), + this.userData = const Value.absent(), + this.rowid = const Value.absent(), + }) : userId = Value(userId), + id = Value(id), + syncing = Value(syncing); + static Insertable custom({ + Expression? userId, + Expression? id, + Expression? syncing, + Expression? sortName, + Expression? parentId, + Expression? path, + Expression? fileSize, + Expression? videoFileName, + Expression? trickPlayModel, + Expression? mediaSegments, + Expression? images, + Expression? chapters, + Expression? subtitles, + Expression? userData, + Expression? rowid, + }) { + return RawValuesInsertable({ + if (userId != null) 'user_id': userId, + if (id != null) 'id': id, + if (syncing != null) 'syncing': syncing, + if (sortName != null) 'sort_name': sortName, + if (parentId != null) 'parent_id': parentId, + if (path != null) 'path': path, + if (fileSize != null) 'file_size': fileSize, + if (videoFileName != null) 'video_file_name': videoFileName, + if (trickPlayModel != null) 'trick_play_model': trickPlayModel, + if (mediaSegments != null) 'media_segments': mediaSegments, + if (images != null) 'images': images, + if (chapters != null) 'chapters': chapters, + if (subtitles != null) 'subtitles': subtitles, + if (userData != null) 'user_data': userData, + if (rowid != null) 'rowid': rowid, + }); + } + + DatabaseItemsCompanion copyWith( + {Value? userId, + Value? id, + Value? syncing, + Value? sortName, + Value? parentId, + Value? path, + Value? fileSize, + Value? videoFileName, + Value? trickPlayModel, + Value? mediaSegments, + Value? images, + Value? chapters, + Value? subtitles, + Value? userData, + Value? rowid}) { + return DatabaseItemsCompanion( + userId: userId ?? this.userId, + id: id ?? this.id, + syncing: syncing ?? this.syncing, + sortName: sortName ?? this.sortName, + parentId: parentId ?? this.parentId, + path: path ?? this.path, + fileSize: fileSize ?? this.fileSize, + videoFileName: videoFileName ?? this.videoFileName, + trickPlayModel: trickPlayModel ?? this.trickPlayModel, + mediaSegments: mediaSegments ?? this.mediaSegments, + images: images ?? this.images, + chapters: chapters ?? this.chapters, + subtitles: subtitles ?? this.subtitles, + userData: userData ?? this.userData, + rowid: rowid ?? this.rowid, + ); + } + + @override + Map toColumns(bool nullToAbsent) { + final map = {}; + if (userId.present) { + map['user_id'] = Variable(userId.value); + } + if (id.present) { + map['id'] = Variable(id.value); + } + if (syncing.present) { + map['syncing'] = Variable(syncing.value); + } + if (sortName.present) { + map['sort_name'] = Variable(sortName.value); + } + if (parentId.present) { + map['parent_id'] = Variable(parentId.value); + } + if (path.present) { + map['path'] = Variable(path.value); + } + if (fileSize.present) { + map['file_size'] = Variable(fileSize.value); + } + if (videoFileName.present) { + map['video_file_name'] = Variable(videoFileName.value); + } + if (trickPlayModel.present) { + map['trick_play_model'] = Variable(trickPlayModel.value); + } + if (mediaSegments.present) { + map['media_segments'] = Variable(mediaSegments.value); + } + if (images.present) { + map['images'] = Variable(images.value); + } + if (chapters.present) { + map['chapters'] = Variable(chapters.value); + } + if (subtitles.present) { + map['subtitles'] = Variable(subtitles.value); + } + if (userData.present) { + map['user_data'] = Variable(userData.value); + } + if (rowid.present) { + map['rowid'] = Variable(rowid.value); + } + return map; + } + + @override + String toString() { + return (StringBuffer('DatabaseItemsCompanion(') + ..write('userId: $userId, ') + ..write('id: $id, ') + ..write('syncing: $syncing, ') + ..write('sortName: $sortName, ') + ..write('parentId: $parentId, ') + ..write('path: $path, ') + ..write('fileSize: $fileSize, ') + ..write('videoFileName: $videoFileName, ') + ..write('trickPlayModel: $trickPlayModel, ') + ..write('mediaSegments: $mediaSegments, ') + ..write('images: $images, ') + ..write('chapters: $chapters, ') + ..write('subtitles: $subtitles, ') + ..write('userData: $userData, ') + ..write('rowid: $rowid') + ..write(')')) + .toString(); + } +} + +abstract class _$AppDatabase extends GeneratedDatabase { + _$AppDatabase(QueryExecutor e) : super(e); + $AppDatabaseManager get managers => $AppDatabaseManager(this); + late final $DatabaseItemsTable databaseItems = $DatabaseItemsTable(this); + late final Index databaseId = + Index('database_id', 'CREATE INDEX database_id ON database_items (id)'); + @override + Iterable> get allTables => + allSchemaEntities.whereType>(); + @override + List get allSchemaEntities => + [databaseItems, databaseId]; +} + +typedef $$DatabaseItemsTableCreateCompanionBuilder = DatabaseItemsCompanion + Function({ + required String userId, + required String id, + required bool syncing, + Value sortName, + Value parentId, + Value path, + Value fileSize, + Value videoFileName, + Value trickPlayModel, + Value mediaSegments, + Value images, + Value chapters, + Value subtitles, + Value userData, + Value rowid, +}); +typedef $$DatabaseItemsTableUpdateCompanionBuilder = DatabaseItemsCompanion + Function({ + Value userId, + Value id, + Value syncing, + Value sortName, + Value parentId, + Value path, + Value fileSize, + Value videoFileName, + Value trickPlayModel, + Value mediaSegments, + Value images, + Value chapters, + Value subtitles, + Value userData, + Value rowid, +}); + +class $$DatabaseItemsTableFilterComposer + extends Composer<_$AppDatabase, $DatabaseItemsTable> { + $$DatabaseItemsTableFilterComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnFilters get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnFilters(column)); + + ColumnFilters get syncing => $composableBuilder( + column: $table.syncing, builder: (column) => ColumnFilters(column)); + + ColumnFilters get sortName => $composableBuilder( + column: $table.sortName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get parentId => $composableBuilder( + column: $table.parentId, builder: (column) => ColumnFilters(column)); + + ColumnFilters get path => $composableBuilder( + column: $table.path, builder: (column) => ColumnFilters(column)); + + ColumnFilters get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => ColumnFilters(column)); + + ColumnFilters get videoFileName => $composableBuilder( + column: $table.videoFileName, builder: (column) => ColumnFilters(column)); + + ColumnFilters get trickPlayModel => $composableBuilder( + column: $table.trickPlayModel, + builder: (column) => ColumnFilters(column)); + + ColumnFilters get mediaSegments => $composableBuilder( + column: $table.mediaSegments, builder: (column) => ColumnFilters(column)); + + ColumnFilters get images => $composableBuilder( + column: $table.images, builder: (column) => ColumnFilters(column)); + + ColumnFilters get chapters => $composableBuilder( + column: $table.chapters, builder: (column) => ColumnFilters(column)); + + ColumnFilters get subtitles => $composableBuilder( + column: $table.subtitles, builder: (column) => ColumnFilters(column)); + + ColumnFilters get userData => $composableBuilder( + column: $table.userData, builder: (column) => ColumnFilters(column)); +} + +class $$DatabaseItemsTableOrderingComposer + extends Composer<_$AppDatabase, $DatabaseItemsTable> { + $$DatabaseItemsTableOrderingComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + ColumnOrderings get userId => $composableBuilder( + column: $table.userId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get id => $composableBuilder( + column: $table.id, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get syncing => $composableBuilder( + column: $table.syncing, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get sortName => $composableBuilder( + column: $table.sortName, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get parentId => $composableBuilder( + column: $table.parentId, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get path => $composableBuilder( + column: $table.path, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get fileSize => $composableBuilder( + column: $table.fileSize, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get videoFileName => $composableBuilder( + column: $table.videoFileName, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get trickPlayModel => $composableBuilder( + column: $table.trickPlayModel, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get mediaSegments => $composableBuilder( + column: $table.mediaSegments, + builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get images => $composableBuilder( + column: $table.images, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get chapters => $composableBuilder( + column: $table.chapters, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get subtitles => $composableBuilder( + column: $table.subtitles, builder: (column) => ColumnOrderings(column)); + + ColumnOrderings get userData => $composableBuilder( + column: $table.userData, builder: (column) => ColumnOrderings(column)); +} + +class $$DatabaseItemsTableAnnotationComposer + extends Composer<_$AppDatabase, $DatabaseItemsTable> { + $$DatabaseItemsTableAnnotationComposer({ + required super.$db, + required super.$table, + super.joinBuilder, + super.$addJoinBuilderToRootComposer, + super.$removeJoinBuilderFromRootComposer, + }); + GeneratedColumn get userId => + $composableBuilder(column: $table.userId, builder: (column) => column); + + GeneratedColumn get id => + $composableBuilder(column: $table.id, builder: (column) => column); + + GeneratedColumn get syncing => + $composableBuilder(column: $table.syncing, builder: (column) => column); + + GeneratedColumn get sortName => + $composableBuilder(column: $table.sortName, builder: (column) => column); + + GeneratedColumn get parentId => + $composableBuilder(column: $table.parentId, builder: (column) => column); + + GeneratedColumn get path => + $composableBuilder(column: $table.path, builder: (column) => column); + + GeneratedColumn get fileSize => + $composableBuilder(column: $table.fileSize, builder: (column) => column); + + GeneratedColumn get videoFileName => $composableBuilder( + column: $table.videoFileName, builder: (column) => column); + + GeneratedColumn get trickPlayModel => $composableBuilder( + column: $table.trickPlayModel, builder: (column) => column); + + GeneratedColumn get mediaSegments => $composableBuilder( + column: $table.mediaSegments, builder: (column) => column); + + GeneratedColumn get images => + $composableBuilder(column: $table.images, builder: (column) => column); + + GeneratedColumn get chapters => + $composableBuilder(column: $table.chapters, builder: (column) => column); + + GeneratedColumn get subtitles => + $composableBuilder(column: $table.subtitles, builder: (column) => column); + + GeneratedColumn get userData => + $composableBuilder(column: $table.userData, builder: (column) => column); +} + +class $$DatabaseItemsTableTableManager extends RootTableManager< + _$AppDatabase, + $DatabaseItemsTable, + DatabaseItem, + $$DatabaseItemsTableFilterComposer, + $$DatabaseItemsTableOrderingComposer, + $$DatabaseItemsTableAnnotationComposer, + $$DatabaseItemsTableCreateCompanionBuilder, + $$DatabaseItemsTableUpdateCompanionBuilder, + ( + DatabaseItem, + BaseReferences<_$AppDatabase, $DatabaseItemsTable, DatabaseItem> + ), + DatabaseItem, + PrefetchHooks Function()> { + $$DatabaseItemsTableTableManager(_$AppDatabase db, $DatabaseItemsTable table) + : super(TableManagerState( + db: db, + table: table, + createFilteringComposer: () => + $$DatabaseItemsTableFilterComposer($db: db, $table: table), + createOrderingComposer: () => + $$DatabaseItemsTableOrderingComposer($db: db, $table: table), + createComputedFieldComposer: () => + $$DatabaseItemsTableAnnotationComposer($db: db, $table: table), + updateCompanionCallback: ({ + Value userId = const Value.absent(), + Value id = const Value.absent(), + Value syncing = const Value.absent(), + Value sortName = const Value.absent(), + Value parentId = const Value.absent(), + Value path = const Value.absent(), + Value fileSize = const Value.absent(), + Value videoFileName = const Value.absent(), + Value trickPlayModel = const Value.absent(), + Value mediaSegments = const Value.absent(), + Value images = const Value.absent(), + Value chapters = const Value.absent(), + Value subtitles = const Value.absent(), + Value userData = const Value.absent(), + Value rowid = const Value.absent(), + }) => + DatabaseItemsCompanion( + userId: userId, + id: id, + syncing: syncing, + sortName: sortName, + parentId: parentId, + path: path, + fileSize: fileSize, + videoFileName: videoFileName, + trickPlayModel: trickPlayModel, + mediaSegments: mediaSegments, + images: images, + chapters: chapters, + subtitles: subtitles, + userData: userData, + rowid: rowid, + ), + createCompanionCallback: ({ + required String userId, + required String id, + required bool syncing, + Value sortName = const Value.absent(), + Value parentId = const Value.absent(), + Value path = const Value.absent(), + Value fileSize = const Value.absent(), + Value videoFileName = const Value.absent(), + Value trickPlayModel = const Value.absent(), + Value mediaSegments = const Value.absent(), + Value images = const Value.absent(), + Value chapters = const Value.absent(), + Value subtitles = const Value.absent(), + Value userData = const Value.absent(), + Value rowid = const Value.absent(), + }) => + DatabaseItemsCompanion.insert( + userId: userId, + id: id, + syncing: syncing, + sortName: sortName, + parentId: parentId, + path: path, + fileSize: fileSize, + videoFileName: videoFileName, + trickPlayModel: trickPlayModel, + mediaSegments: mediaSegments, + images: images, + chapters: chapters, + subtitles: subtitles, + userData: userData, + rowid: rowid, + ), + withReferenceMapper: (p0) => p0 + .map((e) => (e.readTable(table), BaseReferences(db, table, e))) + .toList(), + prefetchHooksCallback: null, + )); +} + +typedef $$DatabaseItemsTableProcessedTableManager = ProcessedTableManager< + _$AppDatabase, + $DatabaseItemsTable, + DatabaseItem, + $$DatabaseItemsTableFilterComposer, + $$DatabaseItemsTableOrderingComposer, + $$DatabaseItemsTableAnnotationComposer, + $$DatabaseItemsTableCreateCompanionBuilder, + $$DatabaseItemsTableUpdateCompanionBuilder, + ( + DatabaseItem, + BaseReferences<_$AppDatabase, $DatabaseItemsTable, DatabaseItem> + ), + DatabaseItem, + PrefetchHooks Function()>; + +class $AppDatabaseManager { + final _$AppDatabase _db; + $AppDatabaseManager(this._db); + $$DatabaseItemsTableTableManager get databaseItems => + $$DatabaseItemsTableTableManager(_db, _db.databaseItems); +} diff --git a/lib/models/syncing/i_synced_item.dart b/lib/models/syncing/i_synced_item.dart index 7c82fa8..b612a86 100644 --- a/lib/models/syncing/i_synced_item.dart +++ b/lib/models/syncing/i_synced_item.dart @@ -6,30 +6,13 @@ import 'package:fladder/models/syncing/sync_item.dart'; part 'i_synced_item.g.dart'; -// extension IsarExtensions on String? { -// int get fastHash { -// if (this == null) return 0; -// var hash = 0xcbf29ce484222325; - -// var i = 0; -// while (i < this!.length) { -// final codeUnit = this!.codeUnitAt(i++); -// hash ^= codeUnit >> 8; -// hash *= 0x100000001b3; -// hash ^= codeUnit & 0xFF; -// hash *= 0x100000001b3; -// } - -// return hash; -// } -// } - @collection class ISyncedItem { String? userId; 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..86b38aa 100644 --- a/lib/models/syncing/sync_item.dart +++ b/lib/models/syncing/sync_item.dart @@ -18,7 +18,6 @@ import 'package:fladder/models/items/media_segments_model.dart'; import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/models/syncing/i_synced_item.dart'; -import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/util/localization_helper.dart'; @@ -44,9 +43,12 @@ class SyncedItem with _$SyncedItem { @Default([]) List fChapters, @Default([]) List subtitles, @UserDataJsonSerializer() UserData? userData, + // ignore: invalid_annotation_target + @JsonKey(includeFromJson: false, includeToJson: false) ItemBaseModel? itemModel, }) = _SyncItem; static String trickPlayPath = "TrickPlay"; + static String chaptersPath = "Chapters"; List get chapters => fChapters.map((e) => e.copyWith(imageUrl: joinAll({"$path", e.imageUrl}))).toList(); @@ -69,9 +71,9 @@ class SyncedItem with _$SyncedItem { File get videoFile => File(joinAll(["$path", "$videoFileName"])); Directory get directory => Directory(path ?? ""); - SyncStatus get status => switch (videoFile.existsSync()) { - true => SyncStatus.complete, - _ => SyncStatus.partially, + TaskStatus get status => switch (videoFile.existsSync()) { + true => TaskStatus.complete, + _ => TaskStatus.notFound, }; String? get taskId => task?.taskId; @@ -94,6 +96,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; } @@ -101,10 +104,9 @@ class SyncedItem with _$SyncedItem { return true; } - List nestedChildren(WidgetRef ref) => ref.watch(syncChildrenProvider(this)); - - List getChildren(Ref ref) => ref.read(syncProvider.notifier).getChildren(this); - List getNestedChildren(Ref ref) => ref.read(syncProvider.notifier).getNestedChildren(this); + Future> getChildren(Ref ref) async => await ref.read(syncProvider.notifier).getChildren(this); + Future> getNestedChildren(Ref ref) async => + await ref.read(syncProvider.notifier).getNestedChildren(this); Future get getDirSize async { var files = await directory.list(recursive: true).toList(); @@ -156,44 +158,45 @@ class SyncedItem with _$SyncedItem { } } -enum SyncStatus { - complete( - Color.fromARGB(255, 141, 214, 58), - IconsaxPlusLinear.tick_circle, - ), - partially( - Color.fromARGB(255, 221, 135, 23), - IconsaxPlusLinear.more_circle, - ), - ; - - const SyncStatus(this.color, this.icon); - - final Color color; - String label(BuildContext context) { - return switch (this) { - SyncStatus.partially => context.localized.syncStatusPartially, - SyncStatus.complete => context.localized.syncStatusSynced, - }; - } - - final IconData icon; -} - extension StatusExtension on TaskStatus { - Color color(BuildContext context) => switch (this) { - TaskStatus.enqueued => Colors.blueAccent, - TaskStatus.running => Colors.limeAccent, - TaskStatus.complete => Colors.limeAccent, - TaskStatus.canceled || TaskStatus.notFound || TaskStatus.failed => Theme.of(context).colorScheme.error, - TaskStatus.waitingToRetry => Colors.yellowAccent, - TaskStatus.paused => Colors.orangeAccent, + IconData get icon => switch (this) { + TaskStatus.enqueued => IconsaxPlusLinear.calendar_circle, + TaskStatus.running => IconsaxPlusLinear.arrow_down_1, + TaskStatus.complete => IconsaxPlusLinear.tick_circle, + TaskStatus.notFound => IconsaxPlusLinear.warning_2, + TaskStatus.failed => IconsaxPlusLinear.tag_cross, + TaskStatus.canceled => IconsaxPlusLinear.tag_cross, + TaskStatus.waitingToRetry => IconsaxPlusLinear.clock, + TaskStatus.paused => IconsaxPlusLinear.pause_circle, }; + Color color(BuildContext context) { + final isDarkMode = Theme.of(context).brightness == Brightness.dark; + return isDarkMode + ? switch (this) { + TaskStatus.enqueued => Colors.blueAccent, + TaskStatus.running => Colors.greenAccent, + TaskStatus.complete => Colors.limeAccent, + TaskStatus.notFound => const Color.fromARGB(255, 221, 135, 23), + TaskStatus.canceled || TaskStatus.failed => Theme.of(context).colorScheme.error, + TaskStatus.waitingToRetry => Colors.yellowAccent, + TaskStatus.paused => Colors.tealAccent, + } + : switch (this) { + TaskStatus.enqueued => Colors.blue, + TaskStatus.running => Colors.green, + TaskStatus.complete => Colors.lime, + TaskStatus.notFound => const Color.fromARGB(255, 221, 135, 23), + TaskStatus.canceled || TaskStatus.failed => Theme.of(context).colorScheme.error, + TaskStatus.waitingToRetry => Colors.yellow, + TaskStatus.paused => Colors.teal, + }; + } + String name(BuildContext context) => switch (this) { TaskStatus.enqueued => context.localized.syncStatusEnqueued, TaskStatus.running => context.localized.syncStatusRunning, - TaskStatus.complete => context.localized.syncStatusComplete, + TaskStatus.complete => context.localized.syncStatusSynced, TaskStatus.notFound => context.localized.syncStatusNotFound, TaskStatus.failed => context.localized.syncStatusFailed, TaskStatus.canceled => context.localized.syncStatusCanceled, diff --git a/lib/models/syncing/sync_item.freezed.dart b/lib/models/syncing/sync_item.freezed.dart index bf1e235..b99f71e 100644 --- a/lib/models/syncing/sync_item.freezed.dart +++ b/lib/models/syncing/sync_item.freezed.dart @@ -31,7 +31,10 @@ mixin _$SyncedItem { List get fChapters => throw _privateConstructorUsedError; List get subtitles => throw _privateConstructorUsedError; @UserDataJsonSerializer() - UserData? get userData => throw _privateConstructorUsedError; + UserData? get userData => + throw _privateConstructorUsedError; // ignore: invalid_annotation_target + @JsonKey(includeFromJson: false, includeToJson: false) + ItemBaseModel? get itemModel => throw _privateConstructorUsedError; /// Create a copy of SyncedItem /// with the given fields replaced by the non-null parameter values. @@ -61,7 +64,9 @@ abstract class $SyncedItemCopyWith<$Res> { ImagesData? fImages, List fChapters, List subtitles, - @UserDataJsonSerializer() UserData? userData}); + @UserDataJsonSerializer() UserData? userData, + @JsonKey(includeFromJson: false, includeToJson: false) + ItemBaseModel? itemModel}); $TrickPlayModelCopyWith<$Res>? get fTrickPlayModel; } @@ -96,6 +101,7 @@ class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem> Object? fChapters = null, Object? subtitles = null, Object? userData = freezed, + Object? itemModel = freezed, }) { return _then(_value.copyWith( id: null == id @@ -158,6 +164,10 @@ class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem> ? _value.userData : userData // ignore: cast_nullable_to_non_nullable as UserData?, + itemModel: freezed == itemModel + ? _value.itemModel + : itemModel // ignore: cast_nullable_to_non_nullable + as ItemBaseModel?, ) as $Val); } @@ -199,7 +209,9 @@ abstract class _$$SyncItemImplCopyWith<$Res> ImagesData? fImages, List fChapters, List subtitles, - @UserDataJsonSerializer() UserData? userData}); + @UserDataJsonSerializer() UserData? userData, + @JsonKey(includeFromJson: false, includeToJson: false) + ItemBaseModel? itemModel}); @override $TrickPlayModelCopyWith<$Res>? get fTrickPlayModel; @@ -233,6 +245,7 @@ class __$$SyncItemImplCopyWithImpl<$Res> Object? fChapters = null, Object? subtitles = null, Object? userData = freezed, + Object? itemModel = freezed, }) { return _then(_$SyncItemImpl( id: null == id @@ -295,6 +308,10 @@ class __$$SyncItemImplCopyWithImpl<$Res> ? _value.userData : userData // ignore: cast_nullable_to_non_nullable as UserData?, + itemModel: freezed == itemModel + ? _value.itemModel + : itemModel // ignore: cast_nullable_to_non_nullable + as ItemBaseModel?, )); } } @@ -317,7 +334,8 @@ class _$SyncItemImpl extends _SyncItem { this.fImages, final List fChapters = const [], final List subtitles = const [], - @UserDataJsonSerializer() this.userData}) + @UserDataJsonSerializer() this.userData, + @JsonKey(includeFromJson: false, includeToJson: false) this.itemModel}) : _fChapters = fChapters, _subtitles = subtitles, super._(); @@ -369,10 +387,14 @@ class _$SyncItemImpl extends _SyncItem { @override @UserDataJsonSerializer() final UserData? userData; +// ignore: invalid_annotation_target + @override + @JsonKey(includeFromJson: false, includeToJson: false) + final ItemBaseModel? itemModel; @override String toString() { - return 'SyncedItem(id: $id, syncing: $syncing, parentId: $parentId, userId: $userId, path: $path, markedForDelete: $markedForDelete, sortName: $sortName, fileSize: $fileSize, videoFileName: $videoFileName, mediaSegments: $mediaSegments, fTrickPlayModel: $fTrickPlayModel, fImages: $fImages, fChapters: $fChapters, subtitles: $subtitles, userData: $userData)'; + return 'SyncedItem(id: $id, syncing: $syncing, parentId: $parentId, userId: $userId, path: $path, markedForDelete: $markedForDelete, sortName: $sortName, fileSize: $fileSize, videoFileName: $videoFileName, mediaSegments: $mediaSegments, fTrickPlayModel: $fTrickPlayModel, fImages: $fImages, fChapters: $fChapters, subtitles: $subtitles, userData: $userData, itemModel: $itemModel)'; } /// Create a copy of SyncedItem @@ -400,7 +422,9 @@ abstract class _SyncItem extends SyncedItem { final ImagesData? fImages, final List fChapters, final List subtitles, - @UserDataJsonSerializer() final UserData? userData}) = _$SyncItemImpl; + @UserDataJsonSerializer() final UserData? userData, + @JsonKey(includeFromJson: false, includeToJson: false) + final ItemBaseModel? itemModel}) = _$SyncItemImpl; _SyncItem._() : super._(); @override @@ -433,7 +457,10 @@ abstract class _SyncItem extends SyncedItem { List get subtitles; @override @UserDataJsonSerializer() - UserData? get userData; + UserData? get userData; // ignore: invalid_annotation_target + @override + @JsonKey(includeFromJson: false, includeToJson: false) + ItemBaseModel? get itemModel; /// Create a copy of SyncedItem /// with the given fields replaced by the non-null parameter values. diff --git a/lib/providers/items/episode_details_provider.dart b/lib/providers/items/episode_details_provider.dart index dcbcdd3..b73b7f7 100644 --- a/lib/providers/items/episode_details_provider.dart +++ b/lib/providers/items/episode_details_provider.dart @@ -72,20 +72,16 @@ class EpisodeDetailsProvider extends StateNotifier { } } - void _tryToCreateOfflineState(ItemBaseModel item) { + Future _tryToCreateOfflineState(ItemBaseModel item) async { final syncNotifier = ref.read(syncProvider.notifier); - final episodeModel = syncNotifier.getSyncedItem(item)?.createItemModel(ref) as EpisodeModel?; + final episodeModel = (await syncNotifier.getSyncedItem(item))?.itemModel as EpisodeModel?; if (episodeModel == null) return; - final seriesSyncedItem = syncNotifier.getSyncedItem(episodeModel.parentBaseModel); + final seriesSyncedItem = await syncNotifier.getSyncedItem(episodeModel.parentBaseModel); if (seriesSyncedItem == null) return; - final seriesModel = seriesSyncedItem.createItemModel(ref) as SeriesModel?; + final seriesModel = seriesSyncedItem.itemModel as SeriesModel?; if (seriesModel == null) return; - final episodes = syncNotifier - .getNestedChildren(seriesSyncedItem) - .map( - (e) => e.createItemModel(ref), - ) - .nonNulls + final episodes = (await syncNotifier.getNestedChildren(seriesSyncedItem)) + .map((e) => e.itemModel) .whereType() .toList(); state = state.copyWith( diff --git a/lib/providers/items/movies_details_provider.dart b/lib/providers/items/movies_details_provider.dart index d9b67a8..1bd431a 100644 --- a/lib/providers/items/movies_details_provider.dart +++ b/lib/providers/items/movies_details_provider.dart @@ -35,11 +35,11 @@ class MovieDetails extends _$MovieDetails { } } - void _tryToCreateOfflineState(ItemBaseModel item) { + void _tryToCreateOfflineState(ItemBaseModel item) async { final syncNotifier = ref.read(syncProvider.notifier); - final syncedItem = syncNotifier.getParentItem(item.id); + final syncedItem = await syncNotifier.getParentItem(item.id); if (syncedItem == null) return; - final movieModel = syncedItem.createItemModel(ref) as MovieModel; + final movieModel = syncedItem.itemModel as MovieModel?; state = movieModel; } diff --git a/lib/providers/items/movies_details_provider.g.dart b/lib/providers/items/movies_details_provider.g.dart index e852d19..20c97d9 100644 --- a/lib/providers/items/movies_details_provider.g.dart +++ b/lib/providers/items/movies_details_provider.g.dart @@ -6,7 +6,7 @@ part of 'movies_details_provider.dart'; // RiverpodGenerator // ************************************************************************** -String _$movieDetailsHash() => r'872ea61464ef8493c7e6c559c526377f1c8f6a6d'; +String _$movieDetailsHash() => r'a9d8d2eeb7fa37652f25c1820b5e346efeeb59fc'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/providers/items/season_details_provider.dart b/lib/providers/items/season_details_provider.dart index 53eb070..af6fc90 100644 --- a/lib/providers/items/season_details_provider.dart +++ b/lib/providers/items/season_details_provider.dart @@ -29,7 +29,12 @@ class SeasonDetailsNotifier extends StateNotifier { seriesId: newState?.seriesId ?? "", seasonId: newState?.id, season: newState?.season, - fields: [ItemFields.overview], + fields: [ + ItemFields.overview, + ItemFields.candelete, + ItemFields.candownload, + ItemFields.parentid, + ], ); newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref).toList()); state = newState; diff --git a/lib/providers/items/series_details_provider.dart b/lib/providers/items/series_details_provider.dart index 61083b0..36e52a0 100644 --- a/lib/providers/items/series_details_provider.dart +++ b/lib/providers/items/series_details_provider.dart @@ -32,20 +32,39 @@ 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, + ItemFields.childcount, ]); + final newEpisodes = EpisodeModel.episodesFromDto( + episodes.body?.items, + ref, + ); + final episodesCanDownload = newEpisodes.any((episode) => episode.canDownload == true); + final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id, fields: [ + ItemFields.mediastreams, + ItemFields.mediasources, + ItemFields.overview, + ItemFields.candownload, + ItemFields.childcount, + ]); newState = newState.copyWith( - availableEpisodes: EpisodeModel.episodesFromDto( - episodes.body?.items, - ref, - ), + seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref) + .map((element) => element.copyWith( + canDownload: true, + episodes: newEpisodes.where((episode) => episode.season == element.season).toList(), + )) + .toList(), + ); + + newState = newState.copyWith( + canDownload: episodesCanDownload, + availableEpisodes: newEpisodes, ); final related = await ref.read(relatedUtilityProvider).relatedContent(seriesModel.id); @@ -59,20 +78,16 @@ class SeriesDetailViewNotifier extends StateNotifier { Future _tryToCreateOfflineState(ItemBaseModel series) async { final syncNotifier = ref.read(syncProvider.notifier); - final syncedItem = syncNotifier.getSyncedItem(series); + final syncedItem = await syncNotifier.getSyncedItem(series); if (syncedItem == null) return; - final seriesModel = syncedItem.createItemModel(ref) as SeriesModel; - final allChildren = syncedItem - .getNestedChildren(ref) - .map( - (e) => e.createItemModel(ref), - ) - .nonNulls - .toList(); - state = seriesModel.copyWith( - availableEpisodes: allChildren.whereType().toList(), - seasons: allChildren.whereType().toList(), - ); + final seriesModel = syncedItem.itemModel as SeriesModel; + final allChildren = (await syncedItem.getNestedChildren(ref)).map((e) => e.itemModel).toList(); + if (mounted) { + state = seriesModel.copyWith( + availableEpisodes: allChildren.whereType().toList(), + seasons: allChildren.whereType().toList(), + ); + } return; } diff --git a/lib/providers/service_provider.dart b/lib/providers/service_provider.dart index 60636da..a718ab2 100644 --- a/lib/providers/service_provider.dart +++ b/lib/providers/service_provider.dart @@ -536,7 +536,10 @@ class JellyService { return api.showsSeriesIdEpisodesGet( seriesId: seriesId, userId: account?.id, - fields: fields, + fields: [ + ...?fields, + ItemFields.parentid, + ], isMissing: isMissing, limit: limit, sortBy: sortBy, @@ -694,7 +697,10 @@ class JellyService { seriesId: seriesId, isMissing: isMissing, enableUserData: enableUserData, - fields: fields, + fields: [ + ...?fields, + ItemFields.parentid, + ], ); Future> itemsFilters2Get({ @@ -903,37 +909,37 @@ class JellyService { Future> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId); -Future _updateUserConfiguration(UserConfiguration newUserConfiguration) async { - if (account?.id == null) return null; + Future _updateUserConfiguration(UserConfiguration newUserConfiguration) async { + if (account?.id == null) return null; - final response = await api.usersConfigurationPost( - userId: account!.id, - body: newUserConfiguration, - ); + final response = await api.usersConfigurationPost( + userId: account!.id, + body: newUserConfiguration, + ); - if (response.isSuccessful) { - return newUserConfiguration; + if (response.isSuccessful) { + return newUserConfiguration; + } + return null; } - return null; -} -Future updateRememberAudioSelections() { - final currentUserConfiguration = account?.userConfiguration; - if (currentUserConfiguration == null) return Future.value(null); + Future updateRememberAudioSelections() { + final currentUserConfiguration = account?.userConfiguration; + if (currentUserConfiguration == null) return Future.value(null); - final updated = currentUserConfiguration.copyWith( - rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false), - ); - return _updateUserConfiguration(updated); -} + final updated = currentUserConfiguration.copyWith( + rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false), + ); + return _updateUserConfiguration(updated); + } -Future updateRememberSubtitleSelections() { - final current = account?.userConfiguration; - if (current == null) return Future.value(null); + Future updateRememberSubtitleSelections() { + final current = account?.userConfiguration; + if (current == null) return Future.value(null); - final updated = current.copyWith( - rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false), - ); - return _updateUserConfiguration(updated); -} + final updated = current.copyWith( + rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false), + ); + return _updateUserConfiguration(updated); + } } 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..010018e 100644 --- a/lib/providers/sync/sync_provider_helpers.dart +++ b/lib/providers/sync/sync_provider_helpers.dart @@ -1,40 +1,41 @@ -import 'package:isar/isar.dart'; +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart'; +import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/syncing/download_stream.dart'; -import 'package:fladder/models/syncing/i_synced_item.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/providers/sync_provider.dart'; 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]; - 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(); - } - return allChildren; +Stream syncedItem(Ref ref, ItemBaseModel? item) { + final id = item?.id; + if (id == null || id.isEmpty) { + return Stream.value(null); } + + return ref.watch(syncProvider.notifier).db.getItem(id).watchSingleOrNull(); +} + +@riverpod +class SyncedChildren extends _$SyncedChildren { + @override + FutureOr> build(SyncedItem item) => ref.read(syncProvider.notifier).getChildren(item); +} + +@riverpod +class SyncedNestedChildren extends _$SyncedNestedChildren { + @override + FutureOr> build(SyncedItem item) => ref.read(syncProvider.notifier).getNestedChildren(item); } @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) { @@ -45,60 +46,53 @@ class SyncDownloadStatus extends _$SyncDownloadStatus { int downloadCount = 0; double fullProgress = mainStream.hasDownload ? mainStream.progress : 0.0; + int fullySyncedChildren = 0; + for (var i = 0; i < nestedChildren.length; i++) { final childItem = nestedChildren[i]; final downloadStream = ref.read(downloadTasksProvider(childItem.id)); + if (childItem.videoFile.existsSync()) { + fullySyncedChildren++; + } if (downloadStream.hasDownload) { downloadCount++; fullProgress += downloadStream.progress; - mainStream = mainStream.copyWith(status: downloadStream.status); + mainStream = mainStream.copyWith( + status: mainStream.status != TaskStatus.running ? downloadStream.status : mainStream.status, + ); } } + int syncAbleChildren = nestedChildren.where((element) => element.hasVideoFile).length; + + var fullySynced = nestedChildren.isNotEmpty ? fullySyncedChildren == syncAbleChildren : arg.videoFile.existsSync(); return mainStream.copyWith( + status: fullySynced ? TaskStatus.complete : mainStream.status, progress: fullProgress / downloadCount.clamp(1, double.infinity).toInt(), ); } } -@riverpod -class SyncStatuses extends _$SyncStatuses { - @override - FutureOr build(SyncedItem arg) async { - final nestedChildren = ref.watch(syncChildrenProvider(arg)); - - ref.watch(downloadTasksProvider(arg.id)); - 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; - } - } - if (arg.hasVideoFile && !await arg.videoFile.exists()) { - return SyncStatus.partially; - } - return SyncStatus.complete; - } -} - @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) { + if (element.videoFile.existsSync()) { + 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..82d5456 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 _$syncedItemHash() => r'7b1178ba78529ebf65425aa4cb8b239a28c7914b'; /// Copied from Dart SDK class _SystemHash { @@ -29,39 +29,30 @@ class _SystemHash { } } -abstract class _$SyncChildren - extends BuildlessAutoDisposeNotifier> { - late final SyncedItem arg; +/// See also [syncedItem]. +@ProviderFor(syncedItem) +const syncedItemProvider = SyncedItemFamily(); - List build( - SyncedItem arg, - ); -} +/// See also [syncedItem]. +class SyncedItemFamily extends Family> { + /// See also [syncedItem]. + const SyncedItemFamily(); -/// See also [SyncChildren]. -@ProviderFor(SyncChildren) -const syncChildrenProvider = SyncChildrenFamily(); - -/// See also [SyncChildren]. -class SyncChildrenFamily extends Family> { - /// See also [SyncChildren]. - const SyncChildrenFamily(); - - /// See also [SyncChildren]. - SyncChildrenProvider call( - SyncedItem arg, + /// See also [syncedItem]. + SyncedItemProvider call( + ItemBaseModel? item, ) { - return SyncChildrenProvider( - arg, + return SyncedItemProvider( + item, ); } @override - SyncChildrenProvider getProviderOverride( - covariant SyncChildrenProvider provider, + SyncedItemProvider getProviderOverride( + covariant SyncedItemProvider provider, ) { return call( - provider.arg, + provider.item, ); } @@ -77,81 +68,75 @@ class SyncChildrenFamily extends Family> { _allTransitiveDependencies; @override - String? get name => r'syncChildrenProvider'; + String? get name => r'syncedItemProvider'; } -/// See also [SyncChildren]. -class SyncChildrenProvider - extends AutoDisposeNotifierProviderImpl> { - /// See also [SyncChildren]. - SyncChildrenProvider( - SyncedItem arg, +/// See also [syncedItem]. +class SyncedItemProvider extends AutoDisposeStreamProvider { + /// See also [syncedItem]. + SyncedItemProvider( + ItemBaseModel? item, ) : this._internal( - () => SyncChildren()..arg = arg, - from: syncChildrenProvider, - name: r'syncChildrenProvider', + (ref) => syncedItem( + ref as SyncedItemRef, + item, + ), + from: syncedItemProvider, + name: r'syncedItemProvider', debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product') ? null - : _$syncChildrenHash, - dependencies: SyncChildrenFamily._dependencies, + : _$syncedItemHash, + dependencies: SyncedItemFamily._dependencies, allTransitiveDependencies: - SyncChildrenFamily._allTransitiveDependencies, - arg: arg, + SyncedItemFamily._allTransitiveDependencies, + item: item, ); - SyncChildrenProvider._internal( + SyncedItemProvider._internal( super._createNotifier, { required super.name, required super.dependencies, required super.allTransitiveDependencies, required super.debugGetCreateSourceHash, required super.from, - required this.arg, + required this.item, }) : super.internal(); - final SyncedItem arg; + final ItemBaseModel? item; @override - List runNotifierBuild( - covariant SyncChildren notifier, + Override overrideWith( + Stream Function(SyncedItemRef provider) create, ) { - return notifier.build( - arg, - ); - } - - @override - Override overrideWith(SyncChildren Function() create) { return ProviderOverride( origin: this, - override: SyncChildrenProvider._internal( - () => create()..arg = arg, + override: SyncedItemProvider._internal( + (ref) => create(ref as SyncedItemRef), from: from, name: null, dependencies: null, allTransitiveDependencies: null, debugGetCreateSourceHash: null, - arg: arg, + item: item, ), ); } @override - AutoDisposeNotifierProviderElement> - createElement() { - return _SyncChildrenProviderElement(this); + AutoDisposeStreamProviderElement createElement() { + return _SyncedItemProviderElement(this); } @override bool operator ==(Object other) { - return other is SyncChildrenProvider && other.arg == arg; + return other is SyncedItemProvider && other.item == item; } @override int get hashCode { var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, arg.hashCode); + hash = _SystemHash.combine(hash, item.hashCode); return _SystemHash.finish(hash); } @@ -159,29 +144,325 @@ 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; +mixin SyncedItemRef on AutoDisposeStreamProviderRef { + /// The parameter `item` of this provider. + ItemBaseModel? get item; } -class _SyncChildrenProviderElement - extends AutoDisposeNotifierProviderElement> - with SyncChildrenRef { - _SyncChildrenProviderElement(super.provider); +class _SyncedItemProviderElement + extends AutoDisposeStreamProviderElement with SyncedItemRef { + _SyncedItemProviderElement(super.provider); @override - SyncedItem get arg => (origin as SyncChildrenProvider).arg; + ItemBaseModel? get item => (origin as SyncedItemProvider).item; +} + +String _$syncedChildrenHash() => r'2b6ce1611750785060df6317ce0ea25e2dc0aeb4'; + +abstract class _$SyncedChildren + extends BuildlessAutoDisposeAsyncNotifier> { + late final SyncedItem item; + + FutureOr> build( + SyncedItem item, + ); +} + +/// See also [SyncedChildren]. +@ProviderFor(SyncedChildren) +const syncedChildrenProvider = SyncedChildrenFamily(); + +/// See also [SyncedChildren]. +class SyncedChildrenFamily extends Family>> { + /// See also [SyncedChildren]. + const SyncedChildrenFamily(); + + /// See also [SyncedChildren]. + SyncedChildrenProvider call( + SyncedItem item, + ) { + return SyncedChildrenProvider( + item, + ); + } + + @override + SyncedChildrenProvider getProviderOverride( + covariant SyncedChildrenProvider provider, + ) { + return call( + provider.item, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'syncedChildrenProvider'; +} + +/// See also [SyncedChildren]. +class SyncedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl< + SyncedChildren, List> { + /// See also [SyncedChildren]. + SyncedChildrenProvider( + SyncedItem item, + ) : this._internal( + () => SyncedChildren()..item = item, + from: syncedChildrenProvider, + name: r'syncedChildrenProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$syncedChildrenHash, + dependencies: SyncedChildrenFamily._dependencies, + allTransitiveDependencies: + SyncedChildrenFamily._allTransitiveDependencies, + item: item, + ); + + SyncedChildrenProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.item, + }) : super.internal(); + + final SyncedItem item; + + @override + FutureOr> runNotifierBuild( + covariant SyncedChildren notifier, + ) { + return notifier.build( + item, + ); + } + + @override + Override overrideWith(SyncedChildren Function() create) { + return ProviderOverride( + origin: this, + override: SyncedChildrenProvider._internal( + () => create()..item = item, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + item: item, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement> + createElement() { + return _SyncedChildrenProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SyncedChildrenProvider && other.item == item; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, item.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SyncedChildrenRef + on AutoDisposeAsyncNotifierProviderRef> { + /// The parameter `item` of this provider. + SyncedItem get item; +} + +class _SyncedChildrenProviderElement + extends AutoDisposeAsyncNotifierProviderElement> with SyncedChildrenRef { + _SyncedChildrenProviderElement(super.provider); + + @override + SyncedItem get item => (origin as SyncedChildrenProvider).item; +} + +String _$syncedNestedChildrenHash() => + r'ea8dd0e694efa6d6ec0c73d699b5fb3e933f9322'; + +abstract class _$SyncedNestedChildren + extends BuildlessAutoDisposeAsyncNotifier> { + late final SyncedItem item; + + FutureOr> build( + SyncedItem item, + ); +} + +/// See also [SyncedNestedChildren]. +@ProviderFor(SyncedNestedChildren) +const syncedNestedChildrenProvider = SyncedNestedChildrenFamily(); + +/// See also [SyncedNestedChildren]. +class SyncedNestedChildrenFamily extends Family>> { + /// See also [SyncedNestedChildren]. + const SyncedNestedChildrenFamily(); + + /// See also [SyncedNestedChildren]. + SyncedNestedChildrenProvider call( + SyncedItem item, + ) { + return SyncedNestedChildrenProvider( + item, + ); + } + + @override + SyncedNestedChildrenProvider getProviderOverride( + covariant SyncedNestedChildrenProvider provider, + ) { + return call( + provider.item, + ); + } + + static const Iterable? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'syncedNestedChildrenProvider'; +} + +/// See also [SyncedNestedChildren]. +class SyncedNestedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl< + SyncedNestedChildren, List> { + /// See also [SyncedNestedChildren]. + SyncedNestedChildrenProvider( + SyncedItem item, + ) : this._internal( + () => SyncedNestedChildren()..item = item, + from: syncedNestedChildrenProvider, + name: r'syncedNestedChildrenProvider', + debugGetCreateSourceHash: + const bool.fromEnvironment('dart.vm.product') + ? null + : _$syncedNestedChildrenHash, + dependencies: SyncedNestedChildrenFamily._dependencies, + allTransitiveDependencies: + SyncedNestedChildrenFamily._allTransitiveDependencies, + item: item, + ); + + SyncedNestedChildrenProvider._internal( + super._createNotifier, { + required super.name, + required super.dependencies, + required super.allTransitiveDependencies, + required super.debugGetCreateSourceHash, + required super.from, + required this.item, + }) : super.internal(); + + final SyncedItem item; + + @override + FutureOr> runNotifierBuild( + covariant SyncedNestedChildren notifier, + ) { + return notifier.build( + item, + ); + } + + @override + Override overrideWith(SyncedNestedChildren Function() create) { + return ProviderOverride( + origin: this, + override: SyncedNestedChildrenProvider._internal( + () => create()..item = item, + from: from, + name: null, + dependencies: null, + allTransitiveDependencies: null, + debugGetCreateSourceHash: null, + item: item, + ), + ); + } + + @override + AutoDisposeAsyncNotifierProviderElement> createElement() { + return _SyncedNestedChildrenProviderElement(this); + } + + @override + bool operator ==(Object other) { + return other is SyncedNestedChildrenProvider && other.item == item; + } + + @override + int get hashCode { + var hash = _SystemHash.combine(0, runtimeType.hashCode); + hash = _SystemHash.combine(hash, item.hashCode); + + return _SystemHash.finish(hash); + } +} + +@Deprecated('Will be removed in 3.0. Use Ref instead') +// ignore: unused_element +mixin SyncedNestedChildrenRef + on AutoDisposeAsyncNotifierProviderRef> { + /// The parameter `item` of this provider. + SyncedItem get item; +} + +class _SyncedNestedChildrenProviderElement + extends AutoDisposeAsyncNotifierProviderElement> with SyncedNestedChildrenRef { + _SyncedNestedChildrenProviderElement(super.provider); + + @override + SyncedItem get item => (origin as SyncedNestedChildrenProvider).item; } String _$syncDownloadStatusHash() => - r'5a0f8537a977c52e6083bd84265631ea5d160637'; + r'6ee039e094f1e007ebaeb20ae63430be829cdeb7'; abstract class _$SyncDownloadStatus extends BuildlessAutoDisposeNotifier { late final SyncedItem arg; + late final List children; DownloadStream? build( SyncedItem arg, + List children, ); } @@ -197,9 +478,11 @@ class SyncDownloadStatusFamily extends Family { /// See also [SyncDownloadStatus]. SyncDownloadStatusProvider call( SyncedItem arg, + List children, ) { return SyncDownloadStatusProvider( arg, + children, ); } @@ -209,6 +492,7 @@ class SyncDownloadStatusFamily extends Family { ) { return call( provider.arg, + provider.children, ); } @@ -233,8 +517,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 +532,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< allTransitiveDependencies: SyncDownloadStatusFamily._allTransitiveDependencies, arg: arg, + children: children, ); SyncDownloadStatusProvider._internal( @@ -255,9 +543,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 +555,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl< ) { return notifier.build( arg, + children, ); } @@ -273,13 +564,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 +586,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 +606,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,161 +618,20 @@ class _SyncDownloadStatusProviderElement @override SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg; + @override + List get children => + (origin as SyncDownloadStatusProvider).children; } -String _$syncStatusesHash() => r'f05ee53368d1de130714bba09132e08aba15bc44'; - -abstract class _$SyncStatuses - extends BuildlessAutoDisposeAsyncNotifier { - late final SyncedItem arg; - - FutureOr build( - SyncedItem arg, - ); -} - -/// See also [SyncStatuses]. -@ProviderFor(SyncStatuses) -const syncStatusesProvider = SyncStatusesFamily(); - -/// See also [SyncStatuses]. -class SyncStatusesFamily extends Family> { - /// See also [SyncStatuses]. - const SyncStatusesFamily(); - - /// See also [SyncStatuses]. - SyncStatusesProvider call( - SyncedItem arg, - ) { - return SyncStatusesProvider( - arg, - ); - } - - @override - SyncStatusesProvider getProviderOverride( - covariant SyncStatusesProvider provider, - ) { - return call( - provider.arg, - ); - } - - static const Iterable? _dependencies = null; - - @override - Iterable? get dependencies => _dependencies; - - static const Iterable? _allTransitiveDependencies = null; - - @override - Iterable? get allTransitiveDependencies => - _allTransitiveDependencies; - - @override - String? get name => r'syncStatusesProvider'; -} - -/// See also [SyncStatuses]. -class SyncStatusesProvider - extends AutoDisposeAsyncNotifierProviderImpl { - /// See also [SyncStatuses]. - SyncStatusesProvider( - SyncedItem arg, - ) : this._internal( - () => SyncStatuses()..arg = arg, - from: syncStatusesProvider, - name: r'syncStatusesProvider', - debugGetCreateSourceHash: - const bool.fromEnvironment('dart.vm.product') - ? null - : _$syncStatusesHash, - dependencies: SyncStatusesFamily._dependencies, - allTransitiveDependencies: - SyncStatusesFamily._allTransitiveDependencies, - arg: arg, - ); - - SyncStatusesProvider._internal( - super._createNotifier, { - required super.name, - required super.dependencies, - required super.allTransitiveDependencies, - required super.debugGetCreateSourceHash, - required super.from, - required this.arg, - }) : super.internal(); - - final SyncedItem arg; - - @override - FutureOr runNotifierBuild( - covariant SyncStatuses notifier, - ) { - return notifier.build( - arg, - ); - } - - @override - Override overrideWith(SyncStatuses Function() create) { - return ProviderOverride( - origin: this, - override: SyncStatusesProvider._internal( - () => create()..arg = arg, - from: from, - name: null, - dependencies: null, - allTransitiveDependencies: null, - debugGetCreateSourceHash: null, - arg: arg, - ), - ); - } - - @override - AutoDisposeAsyncNotifierProviderElement - createElement() { - return _SyncStatusesProviderElement(this); - } - - @override - bool operator ==(Object other) { - return other is SyncStatusesProvider && other.arg == arg; - } - - @override - int get hashCode { - var hash = _SystemHash.combine(0, runtimeType.hashCode); - hash = _SystemHash.combine(hash, arg.hashCode); - - return _SystemHash.finish(hash); - } -} - -@Deprecated('Will be removed in 3.0. Use Ref instead') -// ignore: unused_element -mixin SyncStatusesRef on AutoDisposeAsyncNotifierProviderRef { - /// The parameter `arg` of this provider. - SyncedItem get arg; -} - -class _SyncStatusesProviderElement - extends AutoDisposeAsyncNotifierProviderElement - with SyncStatusesRef { - _SyncStatusesProviderElement(super.provider); - - @override - SyncedItem get arg => (origin as SyncStatusesProvider).arg; -} - -String _$syncSizeHash() => r'138702f2dd69ab28d142bab67ab4a497bb24f252'; +String _$syncSizeHash() => r'eeb6ab8dc1fdf5696c06e53f04a0e54ad68c6748'; abstract class _$SyncSize extends BuildlessAutoDisposeNotifier { late final SyncedItem arg; + late final List? children; int? build( SyncedItem arg, + List? children, ); } @@ -488,9 +647,11 @@ class SyncSizeFamily extends Family { /// See also [SyncSize]. SyncSizeProvider call( SyncedItem arg, + List? children, ) { return SyncSizeProvider( arg, + children, ); } @@ -500,6 +661,7 @@ class SyncSizeFamily extends Family { ) { return call( provider.arg, + provider.children, ); } @@ -523,8 +685,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 +699,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { dependencies: SyncSizeFamily._dependencies, allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies, arg: arg, + children: children, ); SyncSizeProvider._internal( @@ -544,9 +710,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 +722,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl { ) { return notifier.build( arg, + children, ); } @@ -562,13 +731,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 +752,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 +772,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 +784,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..aff5414 100644 --- a/lib/providers/sync_provider.dart +++ b/lib/providers/sync_provider.dart @@ -8,10 +8,10 @@ import 'package:flutter/material.dart'; import 'package:background_downloader/background_downloader.dart'; import 'package:collection/collection.dart'; +import 'package:drift_db_viewer/drift_db_viewer.dart'; import 'package:file_picker/file_picker.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:http/http.dart' as http; -import 'package:isar/isar.dart'; import 'package:path/path.dart' as path; import 'package:path_provider/path_provider.dart'; @@ -20,12 +20,14 @@ import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/models/items/item_shared_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/database_item.dart'; import 'package:fladder/models/syncing/download_stream.dart'; -import 'package:fladder/models/syncing/i_synced_item.dart'; import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_settings_model.dart'; import 'package:fladder/models/video_stream_model.dart'; @@ -37,16 +39,27 @@ import 'package:fladder/providers/sync/background_download_provider.dart'; import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/migration/isar_drift_migration.dart'; final syncProvider = StateNotifierProvider((ref) => throw UnimplementedError()); final downloadTasksProvider = StateProvider.family((ref, id) => DownloadStream.empty()); class SyncNotifier extends StateNotifier { - SyncNotifier(this.ref, this.isar, this.mobileDirectory) : super(SyncSettingsModel()) { + SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) { _init(); } + final Ref ref; + late final db = AppDatabase(ref); + final Directory mobileDirectory; + final String subPath = "Synced"; + + void migrateFromIsar() async { + await isarMigration(ref, db, mainDirectory.path); + _initializeQueryStream(); + } + void _init() { cleanupTemporaryFiles(); ref.listen( @@ -54,35 +67,29 @@ class SyncNotifier extends StateNotifier { (previous, next) { if (previous?.id != next?.id) { if (next?.id != null) { - _initializeQueryStream(next?.id ?? ""); + _initializeQueryStream(); } } }, ); - final userId = ref.read(userProvider)?.id; - if (userId != null) { - _initializeQueryStream(userId); - } + _initializeQueryStream(); + + migrateFromIsar(); } - void _initializeQueryStream(String userId) { + void _initializeQueryStream() async { + final userId = ref.read(userProvider)?.id; + if (userId == null) return; _subscription?.cancel(); - final queryStream = getParentSyncItems - ?.userIdEqualTo(userId) - .watch() - .asyncMap((event) => event.map((e) => SyncedItem.fromIsar(e, syncPath ?? "")).toList()); + final queryStream = db.getParentItems.watch(); - final initItems = getParentSyncItems - ?.userIdEqualTo(userId) - .findAll() - .mapIndexed((index, element) => SyncedItem.fromIsar(element, syncPath ?? "")) - .toList(); + final initItems = await db.getParentItems.get(); - state = state.copyWith(items: initItems ?? []); + state = state.copyWith(items: initItems); - _subscription = queryStream?.listen((items) { + _subscription = queryStream.listen((items) { state = state.copyWith(items: items); }); } @@ -116,15 +123,8 @@ class SyncNotifier extends StateNotifier { } } - final Ref ref; - final Isar? isar; - final Directory mobileDirectory; - final String subPath = "Synced"; - StreamSubscription>? _subscription; - IsarCollection? get syncedItems => isar?.iSyncedItems; - late final JellyService api = ref.read(jellyApiProvider); String? get _savePath => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) @@ -150,9 +150,6 @@ class SyncNotifier extends StateNotifier { String? get syncPath => saveDirectory?.path; - QueryBuilder? get getParentSyncItems => - syncedItems?.where().parentIdIsNull(); - Future get directorySize async { if (saveDirectory == null) return 0; var files = await saveDirectory!.list(recursive: true).toList(); @@ -167,59 +164,62 @@ class SyncNotifier extends StateNotifier { } Future refresh() async { - state = state.copyWith( - items: (await getParentSyncItems?.userIdEqualTo(ref.read(userProvider)?.id).findAllAsync()) - ?.map((e) => SyncedItem.fromIsar(e, syncPath ?? "")) - .toList() ?? - []); + state = state.copyWith(items: (await db.getParentItems.get())); } - List getNestedChildren(SyncedItem item) { - final allChildren = []; - List toProcess = [item]; - while (toProcess.isNotEmpty) { - final currentLevel = toProcess.map( - (parent) { - final children = syncedItems?.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(); - } - return allChildren; - } + Future> getNestedChildren(SyncedItem item) async => db.getNestedChildren(item); - List getChildren(SyncedItem item) { - return (syncedItems?.where().parentIdEqualTo(item.id).sortBySortName().findAll()) - ?.map( - (e) => SyncedItem.fromIsar(e, syncPath ?? ""), - ) - .toList() ?? - []; - } + Future> getChildren(SyncedItem root) async => await db.getChildren(root.id).get(); - SyncedItem? getSyncedItem(ItemBaseModel? item) { + Future getSyncedItem(ItemBaseModel? item) async { final id = item?.id; if (id == null) return null; - final newItem = syncedItems?.get(id); - if (newItem == null) return null; - return SyncedItem.fromIsar(newItem, syncPath ?? ""); + return await db.getItem(id).getSingleOrNull(); } - SyncedItem? getParentItem(String id) { - ISyncedItem? newItem = syncedItems?.get(id); - while (newItem?.parentId != null) { - newItem = syncedItems?.get(newItem!.parentId!); + Future getParentItem(String id) async => await db.getParent(id).getSingleOrNull(); + + Future refreshSyncItem(SyncedItem item) async { + List itemsToSync = await getNestedChildren(item); + + itemsToSync = [item, ...itemsToSync]; + + SyncedItem parentItem = item; + + List newItems = []; + + for (var i = 0; i < itemsToSync.length; i++) { + final itemToSync = itemsToSync[i]; + final itemResponse = await api.usersUserIdItemsItemIdGetBaseItem( + itemId: itemToSync.id, + ); + + final itemModel = ItemBaseModel.fromBaseDto(itemResponse.bodyOrThrow, ref); + + final syncedParent = await db.getItem(itemToSync.parentId ?? "").getSingleOrNull(); + + SyncedItem newSyncedItem = await _syncItemData(syncedParent, itemModel, itemResponse.bodyOrThrow); + + final updatedItem = itemToSync.copyWith( + itemModel: newSyncedItem.createItemModel(ref), + sortName: newSyncedItem.sortName, + syncing: false, + fImages: newSyncedItem.fImages, + fTrickPlayModel: newSyncedItem.fTrickPlayModel, + subtitles: newSyncedItem.subtitles, + userData: UserData.determineLastUserData([item.userData, newSyncedItem.userData]), + ); + + newItems.add(updatedItem); + + if (itemToSync.id == parentItem.id) { + parentItem = updatedItem; + } } - if (newItem == null) return null; - return SyncedItem.fromIsar(newItem, syncPath ?? ""); - } - ItemBaseModel? getItem(SyncedItem? syncedItem) { - if (syncedItem == null) return null; - return syncedItem.createItemModel(ref); + await db.insertMultipleEntries(newItems); + + return parentItem; } Future addSyncItem(BuildContext? context, ItemBaseModel item) async { @@ -228,7 +228,7 @@ class SyncNotifier extends StateNotifier { if (saveDirectory == null) { String? selectedDirectory = await FilePicker.platform.getDirectoryPath(dialogTitle: context.localized.syncSelectDownloadsFolder); - if (selectedDirectory?.isEmpty == true) { + if (selectedDirectory?.isEmpty == true && context.mounted) { fladderSnackbar(context, title: context.localized.syncNoFolderSetup); return; } @@ -238,22 +238,29 @@ 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 }; - fladderSnackbar(context, - title: newSync != null - ? context.localized.startedSyncingItem(item.detailedName(context) ?? "Unknown") - : context.localized.unableToSyncItem(item.detailedName(context) ?? "Unknown")); + if (context.mounted) { + fladderSnackbar(context, + title: newSync != null + ? context.localized.startedSyncingItem(item.detailedName(context) ?? "Unknown") + : context.localized.unableToSyncItem(item.detailedName(context) ?? "Unknown")); + } + return; } + void viewDatabase(BuildContext context) => + Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) => DriftDbViewer(db))); + Future removeSync(BuildContext context, SyncedItem? item) async { try { if (item == null) return false; - final nestedChildren = getNestedChildren(item); + final nestedChildren = await getNestedChildren(item); state = state.copyWith( items: state.items @@ -266,13 +273,7 @@ class SyncNotifier extends StateNotifier { await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!); } - final deleteFromDatabase = isar?.write((isar) => syncedItems?.deleteAll([...nestedChildren, item] - .map( - (e) => e.id, - ) - .toList())); - - if (deleteFromDatabase == 0) return false; + await db.deleteAllItems([...nestedChildren, item]); for (var i = 0; i < nestedChildren.length; i++) { final element = nestedChildren[i]; @@ -367,7 +368,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 +379,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(); @@ -394,12 +395,7 @@ class SyncNotifier extends StateNotifier { return data?.copyWith(path: fileName); } - void updateItemSync(SyncedItem syncedItem) => - isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? ""))); - - Future updateItem(SyncedItem syncedItem) async { - isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? ""))); - } + Future updateItem(SyncedItem syncedItem) async => db.insertItem(syncedItem); Future deleteFullSyncFiles(SyncedItem syncedItem, DownloadTask? task) async { await syncedItem.deleteDatFiles(ref); @@ -415,7 +411,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 +435,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 +444,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 +488,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()); } }, @@ -508,7 +506,7 @@ class SyncNotifier extends StateNotifier { Future clear() async { await mainDirectory.delete(recursive: true); - isar?.write((isar) => syncedItems?.clear()); + await db.clearDatabase(); state = state.copyWith(items: []); } @@ -522,6 +520,24 @@ extension SyncNotifierHelpers on SyncNotifier { Future createSyncItem(BaseItemDto response, {SyncedItem? parent}) async { final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref); + final existingSyncedItem = await getSyncedItem(item); + + if (existingSyncedItem != null) return existingSyncedItem; + + SyncedItem syncItem = await _syncItemData(parent, item, response); + + if (parent == null) { + await db.insertItem(syncItem); + } + + return syncItem.copyWith( + fileSize: response.mediaSources?.firstOrNull?.size ?? 0, + syncing: false, + videoFileName: response.path?.split('/').lastOrNull ?? "", + ); + } + + Future _syncItemData(SyncedItem? parent, ItemBaseModel item, BaseItemDto response) async { final Directory? parentDirectory = parent?.directory; final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id])); @@ -542,30 +558,7 @@ extension SyncNotifierHelpers on SyncNotifier { path: directory.path, 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(); - } + return syncItem; } Future syncMovie(ItemBaseModel item, {bool skipDownload = false}) async { @@ -580,27 +573,28 @@ extension SyncNotifierHelpers on SyncNotifier { if (!syncItem.directory.existsSync()) return null; - await syncVideoFile(syncItem, skipDownload); + await db.insertItem(syncItem); - isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath))); + await syncFile(syncItem, skipDownload); 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); if (!seriesItem.directory.existsSync()) return null; final seasonsResponse = await api.showsSeriesIdSeasonsGet( + seriesId: item.id, isMissing: false, enableUserData: true, fields: [ @@ -621,14 +615,13 @@ extension SyncNotifierHelpers on SyncNotifier { ItemFields.chapters, ItemFields.trickplay, ], - seriesId: item.id, ); 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,30 +644,33 @@ 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 && !await newEpisode.videoFile.exists()) { + itemsToDownload.add(newEpisode); } } } - isar?.write( - (isar) => syncedItems?.putAll(newItems - .map( - (e) => ISyncedItem.fromSynced(e, syncPath ?? ""), - ) - .toList()), - ); + await db.insertMultipleEntries(newItems); - if (itemToDownload != null) { - await syncVideoFile(itemToDownload, false); + for (var i = 0; i < itemsToDownload.length; i++) { + final item = itemsToDownload[i]; + //No need to await file sync happens in the background + 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/routes/auto_router.gr.dart b/lib/routes/auto_router.gr.dart index 5ebe950..edd40c6 100644 --- a/lib/routes/auto_router.gr.dart +++ b/lib/routes/auto_router.gr.dart @@ -464,7 +464,7 @@ class SplashRouteArgs { class SyncedRoute extends _i17.PageRouteInfo { SyncedRoute({ _i22.ScrollController? navigationScrollController, - _i22.Key? key, + _i19.Key? key, List<_i17.PageRouteInfo>? children, }) : super( SyncedRoute.name, @@ -498,7 +498,7 @@ class SyncedRouteArgs { final _i22.ScrollController? navigationScrollController; - final _i22.Key? key; + final _i19.Key? key; @override String toString() { diff --git a/lib/screens/details_screens/book_detail_screen.dart b/lib/screens/details_screens/book_detail_screen.dart index 7cb514d..0547502 100644 --- a/lib/screens/details_screens/book_detail_screen.dart +++ b/lib/screens/details_screens/book_detail_screen.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.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/book_model.dart'; import 'package:fladder/models/items/images_models.dart'; diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index 3fb9fff..faff11f 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; @@ -91,7 +92,7 @@ class HomeScreen extends ConsumerWidget { action: () => e.navigate(context), ); case HomeTabs.sync: - if (canDownload) { + if (canDownload && !kIsWeb) { return DestinationModel( label: context.localized.navigationSync, icon: Icon(e.icon), diff --git a/lib/screens/shared/detail_scaffold.dart b/lib/screens/shared/detail_scaffold.dart index dadf07d..7adcbdb 100644 --- a/lib/screens/shared/detail_scaffold.dart +++ b/lib/screens/shared/detail_scaffold.dart @@ -6,7 +6,10 @@ import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/items/images_models.dart'; +import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/routes/auto_router.gr.dart'; +import 'package:fladder/screens/syncing/sync_button.dart'; +import 'package:fladder/screens/syncing/sync_item_details.dart'; import 'package:fladder/theme.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/fladder_image.dart'; @@ -196,6 +199,19 @@ class _DetailScaffoldState extends ConsumerState { mainAxisSize: MainAxisSize.min, children: [ if (widget.item != null) ...[ + ref.watch(syncedItemProvider(widget.item)).when( + error: (error, stackTrace) => const SizedBox.shrink(), + data: (syncedItem) { + if (syncedItem == null) { + return const SizedBox.shrink(); + } + return IconButton( + onPressed: () => showSyncItemDetails(context, syncedItem, ref), + icon: SyncButton(item: widget.item!, syncedItem: syncedItem), + ); + }, + loading: () => const SizedBox.shrink(), + ), Builder( builder: (context) { final newActions = widget.actions?.call(context); diff --git a/lib/screens/shared/fladder_snackbar.dart b/lib/screens/shared/fladder_snackbar.dart index 812c295..692cfdc 100644 --- a/lib/screens/shared/fladder_snackbar.dart +++ b/lib/screens/shared/fladder_snackbar.dart @@ -10,6 +10,7 @@ void fladderSnackbar( bool showCloseButton = false, Duration duration = const Duration(seconds: 3), }) { + if (!context.mounted) return; ScaffoldMessenger.of(context).showSnackBar(SnackBar( content: Text( title, diff --git a/lib/screens/shared/media/components/next_up_episode.dart b/lib/screens/shared/media/components/next_up_episode.dart index 9736e25..1fbc010 100644 --- a/lib/screens/shared/media/components/next_up_episode.dart +++ b/lib/screens/shared/media/components/next_up_episode.dart @@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:fladder/models/items/episode_model.dart'; -import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/details_screens/components/media_stream_information.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; @@ -31,7 +30,7 @@ class NextUpEpisode extends ConsumerWidget { Opacity( opacity: 0.75, child: SelectableText( - "${context.localized.season(1)} ${nextEpisode.season} - ${context.localized.episode(1)} ${nextEpisode.episode}", + nextEpisode.seasonEpisodeLabelFull(context), style: Theme.of(context).textTheme.titleMedium, ), ), @@ -42,13 +41,11 @@ class NextUpEpisode extends ConsumerWidget { const SizedBox(height: 16), LayoutBuilder( builder: (context, constraints) { - final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(nextEpisode); if (constraints.maxWidth < 550) { return Column( children: [ EpisodePoster( episode: nextEpisode, - syncedItem: syncedItem, showLabel: false, onTap: () => nextEpisode.navigateTo(context), actions: const [], @@ -71,7 +68,6 @@ class NextUpEpisode extends ConsumerWidget { maxWidth: MediaQuery.of(context).size.width / 2), child: EpisodePoster( episode: nextEpisode, - syncedItem: syncedItem, showLabel: false, onTap: () => nextEpisode.navigateTo(context), actions: const [], diff --git a/lib/screens/shared/media/episode_details_list.dart b/lib/screens/shared/media/episode_details_list.dart index f2bc2b6..c1d94b3 100644 --- a/lib/screens/shared/media/episode_details_list.dart +++ b/lib/screens/shared/media/episode_details_list.dart @@ -1,16 +1,16 @@ +import 'package:flutter/material.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/providers/settings/client_settings_provider.dart'; -import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; +import 'package:fladder/util/humanize_duration.dart'; import 'package:fladder/util/item_base_model/item_base_model_extensions.dart'; import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; - -import 'package:fladder/models/items/episode_model.dart'; -import 'package:fladder/util/humanize_duration.dart'; enum EpisodeDetailsViewType { list(icon: IconsaxPlusBold.grid_6), @@ -48,14 +48,12 @@ class EpisodeDetailsList extends ConsumerWidget { itemCount: episodes.length, itemBuilder: (context, index) { final episode = episodes[index]; - final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(episode); List children = [ Flexible( flex: 1, child: EpisodePoster( episode: episode, showLabel: false, - syncedItem: syncedItem, actions: episode.generateActions(context, ref), onTap: () => episode.navigateTo(context), isCurrentEpisode: false, diff --git a/lib/screens/shared/media/episode_posters.dart b/lib/screens/shared/media/episode_posters.dart index 653a034..a259d2c 100644 --- a/lib/screens/shared/media/episode_posters.dart +++ b/lib/screens/shared/media/episode_posters.dart @@ -3,10 +3,8 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.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/sync_provider_helpers.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'; @@ -88,11 +86,9 @@ class _EpisodePosterState extends ConsumerState { itemBuilder: (context, index) { final episode = episodes[index]; final isCurrentEpisode = index == indexOfCurrent; - final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(episode); return EpisodePoster( episode: episode, blur: allPlayed ? false : indexOfCurrent < index, - syncedItem: syncedItem, onTap: widget.onEpisodeTap != null ? () { widget.onEpisodeTap?.call( @@ -130,7 +126,6 @@ class _EpisodePosterState extends ConsumerState { class EpisodePoster extends ConsumerWidget { final EpisodeModel episode; - final SyncedItem? syncedItem; final bool showLabel; final Function()? onTap; final Function()? onLongPress; @@ -141,7 +136,6 @@ class EpisodePoster extends ConsumerWidget { const EpisodePoster({ super.key, required this.episode, - this.syncedItem, this.showLabel = true, this.onTap, this.onLongPress, @@ -156,7 +150,6 @@ class EpisodePoster extends ConsumerWidget { color: Theme.of(context).colorScheme.surfaceContainerHighest, child: const Icon(Icons.local_movies_outlined), ); - final SyncedItem? iSyncedItem = syncedItem; bool episodeAvailable = episode.status == EpisodeStatus.available; return AspectRatio( aspectRatio: 1.76, @@ -203,15 +196,18 @@ class EpisodePoster extends ConsumerWidget { child: Row( mainAxisAlignment: MainAxisAlignment.end, children: [ - if (iSyncedItem != null) - Consumer(builder: (context, ref, child) { - final SyncStatus syncStatus = - ref.watch(syncStatusesProvider(iSyncedItem)).value ?? SyncStatus.partially; - return StatusCard( - color: syncStatus.color, - child: SyncButton(item: episode, syncedItem: syncedItem), - ); - }), + ref.watch(syncedItemProvider(episode)).when( + error: (error, stackTrace) => const SizedBox.shrink(), + data: (syncedItem) { + if (syncedItem == null) { + return const SizedBox.shrink(); + } + return StatusCard( + child: SyncButton(item: episode, syncedItem: syncedItem), + ); + }, + loading: () => const SizedBox.shrink(), + ), if (episode.userData.isFavourite) const StatusCard( color: Colors.red, @@ -259,7 +255,7 @@ class EpisodePoster extends ConsumerWidget { child: Align( alignment: Alignment.bottomRight, child: PopupMenuButton( - tooltip: "Options", + tooltip: context.localized.options, icon: const Icon( Icons.more_vert, color: Colors.white, diff --git a/lib/screens/shared/media/poster_widget.dart b/lib/screens/shared/media/poster_widget.dart index 4fd4981..79a6404 100644 --- a/lib/screens/shared/media/poster_widget.dart +++ b/lib/screens/shared/media/poster_widget.dart @@ -19,6 +19,7 @@ class PosterWidget extends ConsumerWidget { final int maxLines; final double? aspectRatio; final bool inlineTitle; + final bool underTitle; final Set excludeActions; final List otherActions; final Function(String id, UserData? newData)? onUserDataChanged; @@ -33,6 +34,7 @@ class PosterWidget extends ConsumerWidget { this.heroTag, this.aspectRatio, this.inlineTitle = false, + this.underTitle = true, this.excludeActions = const {}, this.otherActions = const [], this.onUserDataChanged, @@ -64,7 +66,7 @@ class PosterWidget extends ConsumerWidget { onPressed: onPressed, ), ), - if (!inlineTitle) + if (!inlineTitle && underTitle) Column( mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.stretch, diff --git a/lib/screens/shared/media/season_row.dart b/lib/screens/shared/media/season_row.dart index 375692f..bf6ab79 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/sync_provider_helpers.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'; @@ -97,30 +99,48 @@ class SeasonPoster extends ConsumerWidget { alignment: Alignment.topLeft, child: placeHolder(season.name), ), - 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), + Align( + alignment: Alignment.topRight, + child: Row( + mainAxisAlignment: MainAxisAlignment.end, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ref.watch(syncedItemProvider(season)).when( + error: (error, stackTrace) => const SizedBox.shrink(), + data: (syncedItem) { + if (syncedItem == null) { + return const SizedBox.shrink(); + } + return StatusCard( + child: SyncButton(item: season, syncedItem: syncedItem), + ); + }, + loading: () => const SizedBox.shrink(), + ), + if (season.userData.unPlayedItemCount != 0) + 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 + Align( + alignment: Alignment.topRight, + child: StatusCard( + color: Theme.of(context).colorScheme.primary, + child: const Icon( + Icons.check_rounded, + ), + ), ), - ), - ), - ) - else - Align( - alignment: Alignment.topRight, - child: StatusCard( - color: Theme.of(context).colorScheme.primary, - child: const Icon( - Icons.check_rounded, - ), - ), + ], ), + ), LayoutBuilder( builder: (context, constraints) { return FlatButton( @@ -134,7 +154,7 @@ class SeasonPoster extends ConsumerWidget { items: season.generateActions(context, ref).popupMenuItems(useIcons: true)); }, onTap: () => onSeasonPressed?.call(season), - onLongPress: AdaptiveLayout.of(context).inputDevice != InputDevice.touch + onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch ? () { showBottomSheetPill( context: context, diff --git a/lib/screens/syncing/sync_button.dart b/lib/screens/syncing/sync_button.dart index ee44de4..07e0e46 100644 --- a/lib/screens/syncing/sync_button.dart +++ b/lib/screens/syncing/sync_button.dart @@ -1,71 +1,51 @@ +import 'package:flutter/material.dart'; + +import 'package:background_downloader/background_downloader.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 { +class SyncButton extends ConsumerWidget { final ItemBaseModel item; - final SyncedItem? syncedItem; + final SyncedItem syncedItem; const SyncButton({required this.item, required this.syncedItem, super.key}); @override - ConsumerState createState() => _SyncButtonState(); -} + Widget build(BuildContext context, WidgetRef ref) { + final nested = ref.watch(syncedNestedChildrenProvider(syncedItem)); + return nested.when( + loading: () => const SizedBox.shrink(), + error: (err, stack) => const SizedBox.shrink(), + data: (children) { + final download = ref.watch(syncDownloadStatusProvider(syncedItem, children)); + final status = download?.status ?? TaskStatus.notFound; + final progress = download?.progress ?? 0.0; -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; - 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, - ), - ), - if ((progress?.progress ?? 0) > 0) - IgnorePointer( - child: SizedBox.fromSize( + return Stack( + alignment: Alignment.center, + children: [ + Icon( + status == TaskStatus.notFound + ? (progress > 0 ? IconsaxPlusLinear.arrow_down_1 : IconsaxPlusLinear.more_circle) + : status.icon, + color: status.color(context), + size: status == TaskStatus.running && progress > 0 ? 16 : null, + ), + SizedBox.fromSize( size: const Size.fromRadius(10), child: CircularProgressIndicator( strokeCap: StrokeCap.round, - strokeWidth: 2, - color: status?.color, - value: progress?.progress, + strokeWidth: 1.5, + color: status.color(context), + value: status == TaskStatus.running ? progress.clamp(0.0, 1.0) : 0, ), ), - ) - ], + ], + ); + }, ); } } diff --git a/lib/screens/syncing/sync_child_item.dart b/lib/screens/syncing/sync_child_item.dart index 342bb11..506001e 100644 --- a/lib/screens/syncing/sync_child_item.dart +++ b/lib/screens/syncing/sync_child_item.dart @@ -1,11 +1,11 @@ -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/providers/sync_provider.dart'; +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 'widgets/synced_episode_item.dart'; @@ -25,39 +25,31 @@ class _ChildSyncWidgetState extends ConsumerState { @override Widget build(BuildContext context) { - final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); - final hasFile = syncedItem.videoFile.existsSync(); + final baseItem = syncedItem.itemModel; if (baseItem == null) { return Container(); } 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, + ), + _ => Container(), + }, + ), + ], ), ), ), diff --git a/lib/screens/syncing/sync_item_details.dart b/lib/screens/syncing/sync_item_details.dart index 0b4a276..a60585b 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'; @@ -15,26 +13,29 @@ import 'package:fladder/screens/shared/default_alert_dialog.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart'; import 'package:fladder/screens/syncing/sync_child_item.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart'; +import 'package:fladder/screens/syncing/widgets/sync_options_button.dart'; import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart'; import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; +import 'package:fladder/util/refresh_state.dart'; import 'package:fladder/util/size_formatting.dart'; import 'package:fladder/widgets/shared/alert_content.dart'; import 'package:fladder/widgets/shared/icon_button_await.dart'; +import 'package:fladder/widgets/shared/pull_to_refresh.dart'; Future showSyncItemDetails( BuildContext context, SyncedItem syncItem, WidgetRef ref, -) { - return showDialogAdaptive( +) async { + await showDialogAdaptive( context: context, builder: (context) => SyncItemDetails( syncItem: syncItem, ), ); + context.refreshData(); } class SyncItemDetails extends ConsumerStatefulWidget { @@ -50,200 +51,196 @@ class _SyncItemDetailsState extends ConsumerState { @override Widget build(BuildContext context) { - final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); + final baseItem = syncedItem.itemModel; final hasFile = syncedItem.videoFile.existsSync(); - final syncChildren = ref.read(syncProvider.notifier).getChildren(syncedItem); - final downloadTask = ref.read(downloadTasksProvider(syncedItem.id)); - - return SyncStatusOverlay( + final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id)); + final syncedChildren = ref.watch(syncedChildrenProvider(syncedItem)); + final nestedChildren = ref.watch(syncedNestedChildrenProvider(syncedItem)); + return PullToRefresh( + refreshOnStart: false, + onRefresh: () async { + final newItem = await ref.read(syncProvider.notifier).refreshSyncItem(syncedItem); + setState(() { + syncedItem = newItem; + }); + }, + child: SyncStatusOverlay( syncedItem: syncedItem, - child: ActionContent( - title: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Card( - elevation: 1, - child: Padding( - padding: const EdgeInsets.all(12.0), - child: Text(baseItem?.type.label(context) ?? ""), - )), - Text( - context.localized.navigationSync, - style: Theme.of(context).textTheme.titleMedium, - ), - IconButton( - onPressed: () => Navigator.pop(context), - icon: const Icon(IconsaxPlusBold.close_circle), - ) - ], - ), - child: Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - if (baseItem != null) ...{ - Row( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SizedBox( - height: (AdaptiveLayout.poster(context).size * - ref.watch(clientSettingsProvider.select((value) => value.posterSize))) * - 0.6, - child: IgnorePointer( - child: PosterWidget( - aspectRatio: 0.7, - poster: baseItem, - inlineTitle: true, - ), - ), - ), - Expanded( - child: SyncProgressBuilder( - item: syncedItem, - builder: (context, combinedStream) { - return Row( - children: [ - Expanded( - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - baseItem.detailedName(context) ?? "", - style: Theme.of(context).textTheme.titleMedium, - ), - SyncSubtitle(syncItem: syncedItem), - SyncLabel( - label: context.localized - .totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), - status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, - ), - ].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)}%")) - ], - )), - ], - ); - }, - ), - ), - if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile) - IconButtonAwait( - onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false), - icon: const Icon(IconsaxPlusLinear.cloud_change), - ) - else if (hasFile) - IconButtonAwait( - color: Theme.of(context).colorScheme.error, - onPressed: () { - showDefaultAlertDialog( - context, - context.localized.syncRemoveDataTitle, - context.localized.syncRemoveDataDesc, - (context) { - ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task); - Navigator.of(context).pop(); - }, - context.localized.delete, - (context) => Navigator.of(context).pop(), - context.localized.cancel, - ); - }, - icon: const Icon(IconsaxPlusLinear.trash), - ), - ].addInBetween(const SizedBox(width: 16)), - ), - }, - const Divider(), - if (syncChildren.isNotEmpty == true) - Flexible( - child: ListView( - shrinkWrap: true, - children: [ - ...syncChildren.map( - (e) => ChildSyncWidget(syncedChild: e), - ), - ], + child: switch (syncedChildren) { + AsyncValue>(value: final children) => ActionContent( + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Card( + elevation: 1, + child: Padding( + padding: const EdgeInsets.all(12.0), + child: Text(baseItem?.type.label(context) ?? ""), + )), + Text( + context.localized.navigationSync, + style: Theme.of(context).textTheme.titleMedium, ), - ), - ], - ), - actions: [ - if (baseItem is! EpisodeModel) - ElevatedButton( - style: ElevatedButton.styleFrom( - backgroundColor: Theme.of(context).colorScheme.errorContainer, - foregroundColor: Theme.of(context).colorScheme.onErrorContainer, - iconColor: Theme.of(context).colorScheme.onErrorContainer, - ), - onPressed: () { - showDefaultAlertDialog( - context, - context.localized.syncDeleteItemTitle, - context.localized.syncDeleteItemDesc(baseItem?.detailedName(context) ?? ""), - (context) async { - await ref.read(syncProvider.notifier).removeSync(context, syncedItem); - Navigator.pop(context); - Navigator.pop(context); + IconButton( + onPressed: () => Navigator.pop(context), + icon: const Icon(IconsaxPlusBold.close_circle), + ) + ], + ), + child: ListView( + shrinkWrap: true, + children: [ + if (baseItem != null) ...{ + Row( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + spacing: 16, + children: [ + SizedBox( + height: (AdaptiveLayout.poster(context).size * + ref.watch(clientSettingsProvider.select((value) => value.posterSize))) * + 0.6, + child: IgnorePointer( + child: PosterWidget( + aspectRatio: 0.70, + poster: baseItem, + underTitle: false, + ), + ), + ), + Expanded( + child: switch (nestedChildren) { + AsyncValue>(:final value) => Builder( + builder: (context) { + final nestedChildren = value ?? []; + return SyncProgressBuilder( + item: syncedItem, + children: nestedChildren, + builder: (context, combinedStream) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + Flexible( + child: Text( + baseItem.detailedName(context) ?? "", + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Flexible( + child: SyncSubtitle( + syncItem: syncedItem, + children: nestedChildren, + ), + ), + Flexible( + child: Consumer( + builder: (context, ref, child) => SyncLabel( + label: context.localized.totalSize(ref + .watch(syncSizeProvider(syncedItem, nestedChildren)) + .byteFormat ?? + '--'), + status: combinedStream?.status ?? TaskStatus.notFound, + ), + ), + ), + if (combinedStream != null && combinedStream.hasDownload == true) + SyncProgressBar(item: syncedItem, task: combinedStream) + ], + ); + }, + ); + }, + ), + }, + ), + if (syncedItem.hasVideoFile && !hasFile && !downloadTask.hasDownload) + IconButtonAwait( + onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false), + icon: const Icon(IconsaxPlusLinear.cloud_change), + ) + else if (hasFile) + IconButtonAwait( + color: Theme.of(context).colorScheme.error, + onPressed: () { + showDefaultAlertDialog( + context, + context.localized.syncRemoveDataTitle, + context.localized.syncRemoveDataDesc, + (context) { + ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task); + Navigator.of(context).pop(); + }, + context.localized.delete, + (context) => Navigator.of(context).pop(), + context.localized.cancel, + ); + }, + icon: const Icon(IconsaxPlusLinear.trash), + ), + nestedChildren.when( + data: (data) => SyncOptionsButton( + syncedItem: syncedItem, + children: data, + ), + error: (error, stackTrace) => const SizedBox.shrink(), + loading: () => const SizedBox.shrink(), + ) + ], + ), + }, + if (children?.isNotEmpty == true) ...[ + const Divider(), + ...children!.map( + (e) => ChildSyncWidget(syncedChild: e), + ), + ], + ], + ), + actions: [ + if (syncedItem.parentId == null) + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.errorContainer, + foregroundColor: Theme.of(context).colorScheme.onErrorContainer, + iconColor: Theme.of(context).colorScheme.onErrorContainer, + ), + onPressed: () { + showDefaultAlertDialog( + context, + context.localized.syncDeleteItemTitle, + context.localized.syncDeleteItemDesc(baseItem?.detailedName(context) ?? ""), + (localContext) async { + await ref.read(syncProvider.notifier).removeSync(context, syncedItem); + Navigator.pop(localContext); + Navigator.pop(context); + }, + context.localized.delete, + (context) => Navigator.pop(context), + context.localized.cancel, + ); }, - context.localized.delete, - (context) => Navigator.pop(context), - context.localized.cancel, - ); - }, - child: Text(context.localized.delete), - ) - else if (syncedItem.parentId != null) - ElevatedButton( - onPressed: () { - final parentItem = ref.read(syncProvider.notifier).getParentItem(syncedItem.parentId!); - setState(() { - if (parentItem != null) { - syncedItem = parentItem; - } - }); - }, - child: Text(context.localized.syncOpenParent), - ) - ], - )); + child: Text(context.localized.delete), + ) + else if (baseItem?.parentBaseModel != null) + ElevatedButton( + onPressed: () async { + final parentItem = await ref.read(syncProvider.notifier).getSyncedItem(baseItem!.parentBaseModel); + setState(() { + if (parentItem != null) { + syncedItem = parentItem; + } + }); + }, + child: Text(context.localized.syncOpenParent), + ) + ], + ), + }, + ), + ); } } diff --git a/lib/screens/syncing/sync_list_item.dart b/lib/screens/syncing/sync_list_item.dart index c759c29..ffd9829 100644 --- a/lib/screens/syncing/sync_list_item.dart +++ b/lib/screens/syncing/sync_list_item.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; +import 'package:background_downloader/background_downloader.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:iconsax_plus/iconsax_plus.dart'; @@ -12,7 +13,6 @@ import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart'; import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart'; import 'package:fladder/util/fladder_image.dart'; -import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/size_formatting.dart'; @@ -28,7 +28,7 @@ class SyncListItemState extends ConsumerState { @override Widget build(BuildContext context) { final syncedItem = widget.syncedItem; - final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); + final baseItem = syncedItem.itemModel; return Padding( padding: const EdgeInsets.symmetric(vertical: 8), child: SyncStatusOverlay( @@ -66,82 +66,99 @@ class SyncListItemState extends ConsumerState { context.localized.cancel); return false; }, - child: LayoutBuilder(builder: (context, constraints) { - return IntrinsicHeight( - child: InkWell( - onTap: () => baseItem?.navigateTo(context), - child: Padding( - padding: const EdgeInsets.all(8.0), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - ConstrainedBox( - constraints: BoxConstraints(maxHeight: 125, maxWidth: constraints.maxWidth * 0.2), - child: Card( - child: AspectRatio( - aspectRatio: baseItem?.primaryRatio ?? 1.0, - child: FladderImage( - image: baseItem?.getPosters?.primary, - fit: BoxFit.cover, - )), - ), - ), - Expanded( - child: SyncProgressBuilder( - item: syncedItem, - builder: (context, combinedStream) { - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - mainAxisSize: MainAxisSize.min, - children: [ - Flexible( - child: Text( - baseItem?.detailedName(context) ?? "", - maxLines: 3, - overflow: TextOverflow.ellipsis, - style: Theme.of(context).textTheme.titleMedium, - ), - ), - Flexible( - child: SyncSubtitle(syncItem: syncedItem), - ), - Flexible( - child: SyncLabel( - label: context.localized - .totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), - status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, - ), - ), - if (combinedStream != null && combinedStream.hasDownload == true) - SyncProgressBar(item: syncedItem, task: combinedStream) - ].addInBetween(const SizedBox(height: 4)), - ); - }, - ), - ), - Column( - crossAxisAlignment: CrossAxisAlignment.center, - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Card( - elevation: 0, - shadowColor: Colors.transparent, - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), - child: Text(baseItem != null ? baseItem.type.label(context) : ""), - )), - IconButton( - onPressed: () => showSyncItemDetails(context, syncedItem, ref), - icon: const Icon(IconsaxPlusLinear.more_square), + child: LayoutBuilder( + builder: (context, constraints) { + return IntrinsicHeight( + child: InkWell( + onTap: () => baseItem?.navigateTo(context), + child: Padding( + padding: const EdgeInsets.all(8.0), + child: Row( + mainAxisSize: MainAxisSize.min, + spacing: 16, + children: [ + ConstrainedBox( + constraints: BoxConstraints(maxHeight: 125, maxWidth: constraints.maxWidth * 0.2), + child: Card( + child: AspectRatio( + aspectRatio: baseItem?.primaryRatio ?? 1.0, + child: FladderImage( + image: baseItem?.getPosters?.primary, + fit: BoxFit.cover, + )), ), - ], - ), - ].addInBetween(const SizedBox(width: 16)), + ), + Expanded( + child: FutureBuilder( + future: ref.read(syncProvider.notifier).getNestedChildren(syncedItem), + builder: (context, asyncSnapshot) { + final nestedChildren = asyncSnapshot.data ?? []; + return SyncProgressBuilder( + item: syncedItem, + children: nestedChildren, + builder: (context, combinedStream) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + Flexible( + child: Text( + baseItem?.detailedName(context) ?? "", + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Flexible( + child: SyncSubtitle( + syncItem: syncedItem, + children: nestedChildren, + ), + ), + Flexible( + child: Consumer( + builder: (context, ref, child) => SyncLabel( + label: context.localized.totalSize( + ref.watch(syncSizeProvider(syncedItem, nestedChildren)).byteFormat ?? + '--'), + status: combinedStream?.status ?? TaskStatus.notFound, + ), + ), + ), + if (combinedStream != null && combinedStream.hasDownload == true) + SyncProgressBar(item: syncedItem, task: combinedStream) + ], + ); + }, + ); + }, + ), + ), + Column( + crossAxisAlignment: CrossAxisAlignment.center, + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Card( + elevation: 0, + shadowColor: Colors.transparent, + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: Text(baseItem != null ? baseItem.type.label(context) : ""), + )), + IconButton( + onPressed: () => showSyncItemDetails(context, syncedItem, ref), + icon: const Icon(IconsaxPlusLinear.more_square), + ), + ], + ), + ], + ), ), ), - ), - ); - }), + ); + }, + ), ), ), ), diff --git a/lib/screens/syncing/sync_widgets.dart b/lib/screens/syncing/sync_widgets.dart index f416c36..ab31052 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'; @@ -12,28 +12,34 @@ import 'package:fladder/models/syncing/sync_item.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/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; + final TaskStatus status; const SyncLabel({this.label, required this.status, super.key}); @override Widget build(BuildContext context, WidgetRef ref) { return Container( decoration: BoxDecoration( - color: status.color.withValues(alpha: 0.15), + color: status.color(context).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10), ), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), child: Text( - label ?? status.label(context), + label ?? status.name(context), style: Theme.of(context).textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.bold, - color: status.color, + color: status.color(context), ), ), ), @@ -61,6 +67,7 @@ class SyncProgressBar extends ConsumerWidget { Row( crossAxisAlignment: CrossAxisAlignment.center, mainAxisSize: MainAxisSize.min, + spacing: 8, children: [ Flexible( child: LinearProgressIndicator( @@ -72,23 +79,29 @@ class SyncProgressBar extends ConsumerWidget { ), Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")), if (downloadTask != null) ...{ - if (downloadStatus != TaskStatus.paused) + if (downloadStatus != TaskStatus.paused && downloadStatus != TaskStatus.enqueued) 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,41 +111,55 @@ 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 baseItem = syncItem.itemModel; + final syncStatus = ref + .watch(syncDownloadStatusProvider(syncItem, children).select((value) => value?.status ?? TaskStatus.notFound)); return Container( - decoration: - BoxDecoration(color: syncStatus.color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)), + decoration: BoxDecoration( + color: syncStatus.color(context).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)), child: Material( color: const Color.fromARGB(0, 208, 130, 130), - textStyle: - Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, color: syncStatus.color), + textStyle: Theme.of(context) + .textTheme + .bodyMedium + ?.copyWith(fontWeight: FontWeight.bold, color: syncStatus.color(context)), child: Padding( padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), child: switch (baseItem) { - SeriesModel _ => Builder( + SeasonModel _ => Builder( builder: (context) { - final itemBaseModels = children.map((e) => ref.read(syncProvider.notifier).getItem(e)); - final seriesItemsSyncLeft = children.where((element) => element.taskId != null).length; - final seasons = itemBaseModels.whereType().length; + final itemBaseModels = children.map((e) => e.itemModel); final episodes = itemBaseModels.whereType().length; return Text( [ - "${context.localized.season(seasons)}: $seasons", - "${context.localized.episode(seasons)}: $episodes | ${context.localized.sync}: ${children.where((element) => element.videoFile.existsSync()).length}${seriesItemsSyncLeft > 0 ? " | Syncing: $seriesItemsSyncLeft" : ""}" + "${context.localized.episode(2)}: $episodes | ${context.localized.syncStatusSynced}: ${children.where((element) => element.videoFile.existsSync()).length}" ].join('\n'), ); }, ), - _ => Text(syncStatus.label(context)), + SeriesModel _ => Builder( + builder: (context) { + final itemBaseModels = children.map((e) => e.itemModel); + final seasons = itemBaseModels.whereType().length; + final episodes = itemBaseModels.whereType().length; + return Text( + [ + "${context.localized.season(2)}: $seasons", + "${context.localized.episode(2)}: $episodes | ${context.localized.syncStatusSynced}: ${children.where((element) => element.videoFile.existsSync()).length}" + ].join('\n'), + ); + }, + ), + _ => Text(syncStatus.name(context)), }, ), ), diff --git a/lib/screens/syncing/synced_screen.dart b/lib/screens/syncing/synced_screen.dart index fcd7832..c96257a 100644 --- a/lib/screens/syncing/synced_screen.dart +++ b/lib/screens/syncing/synced_screen.dart @@ -1,3 +1,4 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:auto_route/auto_route.dart'; @@ -52,6 +53,30 @@ class _SyncedScreenState extends ConsumerState { ) else const DefaultSliverTopBadding(), + if (kDebugMode) + SliverToBoxAdapter( + child: Padding( + padding: padding, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + spacing: 12, + children: [ + ElevatedButton( + onPressed: () => ref.read(syncProvider.notifier).viewDatabase(context), + child: const Text("View Database"), + ), + ElevatedButton( + onPressed: () => ref.read(syncProvider.notifier).db.clearDatabase(), + child: const Text("Clear drift database"), + ), + ElevatedButton( + onPressed: () => ref.read(syncProvider.notifier).migrateFromIsar(), + child: const Text("Migrate Isar to Drift"), + ), + ], + ), + ), + ), if (items.isNotEmpty) ...[ SliverToBoxAdapter( child: Padding( diff --git a/lib/screens/syncing/widgets/sync_options_button.dart b/lib/screens/syncing/widgets/sync_options_button.dart new file mode 100644 index 0000000..10fd615 --- /dev/null +++ b/lib/screens/syncing/widgets/sync_options_button.dart @@ -0,0 +1,200 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; + +import 'package:background_downloader/background_downloader.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:iconsax_plus/iconsax_plus.dart'; + +import 'package:fladder/models/syncing/sync_item.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/util/localization_helper.dart'; +import 'package:fladder/util/refresh_state.dart'; +import 'package:fladder/widgets/shared/filled_button_await.dart'; + +class SyncOptionsButton extends ConsumerWidget { + final SyncedItem syncedItem; + final List children; + const SyncOptionsButton({ + required this.syncedItem, + required this.children, + super.key, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return PopupMenuButton( + itemBuilder: (context) { + final unSyncedChildren = children.where((element) { + final hasDownload = ref.read(syncDownloadStatusProvider(element, [])); + return element.hasVideoFile && !element.videoFile.existsSync() && hasDownload?.status == TaskStatus.notFound; + }).toList(); + + final syncedChildren = + children.where((element) => element.hasVideoFile && element.videoFile.existsSync()).toList(); + + final syncTasks = children + .map((element) { + final task = ref.read(syncDownloadStatusProvider(element, [])); + if (task?.status != TaskStatus.notFound) { + return task; + } else { + return null; + } + }) + .nonNulls + .toList(); + + final runningTasks = syncTasks.where((element) => element.status == TaskStatus.running).toList(); + final enqueuedTasks = syncTasks.where((element) => element.status == TaskStatus.enqueued).toList(); + final pausedTasks = syncTasks.where((element) => element.status == TaskStatus.paused).toList(); + return [ + PopupMenuItem( + child: Row( + spacing: 12, + children: [ + const Icon(IconsaxPlusLinear.refresh_2), + Text(context.localized.refreshMetadata), + ], + ), + onTap: () => context.refreshData(), + ), + if (children.isNotEmpty) ...[ + const PopupMenuDivider(), + PopupMenuItem( + enabled: unSyncedChildren.isNotEmpty, + child: Row( + spacing: 12, + children: [ + const Icon(IconsaxPlusLinear.cloud_add), + Text(context.localized.syncAllFiles), + ], + ), + onTap: () async => _syncRemainingItems(context, syncedItem, unSyncedChildren, ref), + ), + PopupMenuItem( + enabled: syncedChildren.isNotEmpty, + child: Row( + spacing: 12, + children: [ + const Icon(IconsaxPlusLinear.trash), + Text(context.localized.syncDeleteAll), + ], + ), + onTap: () async => _deleteSyncedItems(context, syncedItem, syncedChildren, ref), + ), + const PopupMenuDivider(), + PopupMenuItem( + enabled: pausedTasks.isNotEmpty, + child: Row( + spacing: 12, + children: [ + const Icon(IconsaxPlusLinear.play), + Text(context.localized.syncResumeAll), + ], + ), + onTap: () => ref + .read(backgroundDownloaderProvider) + .resumeAll(tasks: pausedTasks.map((e) => e.task).nonNulls.toList()), + ), + PopupMenuItem( + enabled: runningTasks.isNotEmpty, + child: Row( + spacing: 12, + children: [ + const Icon(IconsaxPlusLinear.pause), + Text(context.localized.syncPauseAll), + ], + ), + onTap: () { + ref + .read(backgroundDownloaderProvider) + .pauseAll(tasks: runningTasks.map((e) => e.task).nonNulls.toList()); + }, + ), + PopupMenuItem( + enabled: [...runningTasks, ...pausedTasks, ...enqueuedTasks].isNotEmpty, + child: Row( + spacing: 12, + children: [ + const Icon(IconsaxPlusLinear.stop), + Text(context.localized.syncStopAll), + ], + ), + onTap: () { + ref.read(backgroundDownloaderProvider).cancelAll( + tasks: [...runningTasks, ...pausedTasks, ...enqueuedTasks].map((e) => e.task).nonNulls.toList()); + }, + ), + ] + ]; + }, + ); + } +} + +Future _deleteSyncedItems( + BuildContext context, SyncedItem syncedItem, List syncedChildren, WidgetRef ref) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text(context.localized.syncDeleteAllItemsTitle(syncedItem.itemModel?.name ?? "")), + content: Text( + context.localized.syncDeleteAllItemsDesc(syncedItem.itemModel?.name ?? "", syncedChildren.length), + ), + scrollable: true, + actions: [ + ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)), + FilledButtonAwait( + style: FilledButton.styleFrom( + backgroundColor: Theme.of(context).colorScheme.errorContainer, + foregroundColor: Theme.of(context).colorScheme.onErrorContainer, + iconColor: Theme.of(context).colorScheme.onErrorContainer, + ), + onPressed: () async { + final deleteList = syncedChildren.map((e) => ref.read(syncProvider.notifier).deleteFullSyncFiles(e, null)); + await Future.wait(deleteList); + Navigator.of(context).pop(); + }, + child: Text( + context.localized.delete, + ), + ) + ], + ), + ); +} + +Future _syncRemainingItems( + BuildContext context, SyncedItem syncedItem, List unSyncedChildren, WidgetRef ref) { + return showDialog( + context: context, + barrierDismissible: false, + builder: (context) => AlertDialog( + title: Text(context.localized.syncAllItemsTitle(syncedItem.itemModel?.name ?? "")), + content: Text( + context.localized.syncAllItemsDesc( + syncedItem.itemModel?.name ?? "", + unSyncedChildren.length, + ), + ), + scrollable: true, + actions: [ + ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)), + FilledButtonAwait( + onPressed: () async { + final syncList = unSyncedChildren.map((e) => ref.read(syncProvider.notifier).syncFile(e, false)); + await Future.wait(syncList); + Navigator.of(context).pop(); + }, + child: Text( + context.localized.sync, + ), + ) + ], + ), + ); +} 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..1957db8 100644 --- a/lib/screens/syncing/widgets/synced_episode_item.dart +++ b/lib/screens/syncing/widgets/synced_episode_item.dart @@ -1,13 +1,16 @@ import 'package:flutter/material.dart'; -import 'package:iconsax_plus/iconsax_plus.dart'; +import 'package:auto_route/auto_route.dart'; +import 'package:background_downloader/background_downloader.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'; @@ -20,12 +23,10 @@ class SyncedEpisodeItem extends ConsumerStatefulWidget { super.key, required this.episode, required this.syncedItem, - required this.hasFile, }); final EpisodeModel episode; final SyncedItem syncedItem; - final bool hasFile; @override ConsumerState createState() => _SyncedEpisodeItemState(); @@ -38,87 +39,94 @@ class _SyncedEpisodeItemState extends ConsumerState { final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id)); final hasFile = widget.syncedItem.videoFile.existsSync(); - return Row( - children: [ - IgnorePointer( - child: ConstrainedBox( + return IntrinsicHeight( + child: Row( + children: [ + ConstrainedBox( constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3), - child: SizedBox( - width: 250, - child: EpisodePoster( - episode: widget.episode, - syncedItem: syncedItem, - actions: [], - showLabel: false, - isCurrentEpisode: false, + child: FlatButton( + onTap: () { + widget.episode.navigateTo(context); + return context.maybePop(); + }, + child: SizedBox( + width: 175, + child: EpisodePoster( + episode: widget.episode, + actions: [], + showLabel: false, + isCurrentEpisode: false, + ), ), ), ), - ), - Expanded( - child: Column( - mainAxisSize: MainAxisSize.max, - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Column( - mainAxisAlignment: MainAxisAlignment.center, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Text( - widget.episode.name, - style: Theme.of(context).textTheme.titleMedium, - ), - Opacity( - opacity: 0.75, - child: Text( - widget.episode.seasonEpisodeLabel(context), - style: Theme.of(context).textTheme.bodyLarge, + Expanded( + child: Column( + mainAxisSize: MainAxisSize.max, + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + widget.episode.name, + style: Theme.of(context).textTheme.titleMedium, ), - ), - ], - ), - ), - if (!widget.hasFile && downloadTask.hasDownload) - Flexible( - child: SyncProgressBar(item: syncedItem, task: downloadTask), - ) - else - Flexible( - child: SyncLabel( - label: context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), - status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, + Opacity( + opacity: 0.75, + child: Text( + widget.episode.seasonEpisodeLabel(context), + style: Theme.of(context).textTheme.bodyLarge, + ), + ), + ], ), - ) - ], + ), + if (!hasFile && downloadTask.hasDownload) + Flexible( + child: SyncProgressBar(item: syncedItem, task: downloadTask), + ) + else + Flexible( + child: SyncLabel( + label: + context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem, [])).byteFormat ?? '--'), + status: ref.watch(syncDownloadStatusProvider(syncedItem, []) + .select((value) => value?.status ?? TaskStatus.notFound)), + ), + ) + ], + ), ), - ), - if (!hasFile && !downloadTask.hasDownload) - IconButtonAwait( - onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false), - icon: const Icon(IconsaxPlusLinear.cloud_change), - ) - else if (hasFile) - IconButtonAwait( - color: Theme.of(context).colorScheme.error, - onPressed: () async { - await showDefaultAlertDialog( - context, - context.localized.syncRemoveDataTitle, - context.localized.syncRemoveDataDesc, - (context) async { - await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task); - Navigator.pop(context); - }, - context.localized.delete, - (context) => Navigator.pop(context), - context.localized.cancel, - ); - }, - icon: const Icon(IconsaxPlusLinear.trash), - ) - ].addInBetween(const SizedBox(width: 16)), + if (!hasFile && !downloadTask.hasDownload) + IconButtonAwait( + onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false), + icon: const Icon(IconsaxPlusLinear.cloud_change), + ) + else if (hasFile) + IconButtonAwait( + color: Theme.of(context).colorScheme.error, + onPressed: () async { + await showDefaultAlertDialog( + context, + context.localized.syncRemoveDataTitle, + context.localized.syncRemoveDataDesc, + (context) async { + await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task); + Navigator.pop(context); + }, + context.localized.delete, + (context) => Navigator.pop(context), + context.localized.cancel, + ); + }, + icon: const Icon(IconsaxPlusLinear.trash), + ) + ].addInBetween(const SizedBox(width: 16)), + ), ); } } diff --git a/lib/screens/syncing/widgets/synced_season_poster.dart b/lib/screens/syncing/widgets/synced_season_poster.dart index 79ce2b5..4129f73 100644 --- a/lib/screens/syncing/widgets/synced_season_poster.dart +++ b/lib/screens/syncing/widgets/synced_season_poster.dart @@ -1,13 +1,21 @@ +import 'package:flutter/material.dart'; + +import 'package:auto_route/auto_route.dart'; +import 'package:background_downloader/background_downloader.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/shared/animated_fade_size.dart'; +import 'package:fladder/providers/sync/sync_provider_helpers.dart'; +import 'package:fladder/screens/shared/flat_button.dart'; +import 'package:fladder/screens/syncing/sync_widgets.dart'; +import 'package:fladder/screens/syncing/widgets/sync_options_button.dart'; +import 'package:fladder/screens/syncing/widgets/sync_progress_builder.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/util/localization_helper.dart'; +import 'package:fladder/util/size_formatting.dart'; class SyncedSeasonPoster extends ConsumerStatefulWidget { const SyncedSeasonPoster({ @@ -28,68 +36,96 @@ class _SyncedSeasonPosterState extends ConsumerState { @override 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, - child: Card( - child: FladderImage( - image: season.getPosters?.primary ?? - season.parentImages?.backDrop?.firstOrNull ?? - season.parentImages?.primary, + final nestedChildren = ref.watch(syncedNestedChildrenProvider(widget.syncedItem)); + return nestedChildren.when( + data: (children) => Builder( + builder: (context) { + final syncedItem = widget.syncedItem; + return ExpansionTile( + tilePadding: EdgeInsets.zero, + shape: const Border(), + title: Row( + spacing: 12, + 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 ?? + season.parentImages?.backDrop?.firstOrNull ?? + season.parentImages?.primary, + ), + ), + ), + ), + ), + Flexible( + child: SyncProgressBuilder( + item: syncedItem, + children: children, + builder: (context, combinedStream) { + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + spacing: 4, + children: [ + Flexible( + child: Text( + season.name, + maxLines: 3, + overflow: TextOverflow.ellipsis, + style: Theme.of(context).textTheme.titleMedium, + ), + ), + Flexible( + child: SyncSubtitle( + syncItem: syncedItem, + children: children, + ), + ), + Flexible( + child: Consumer( + builder: (context, ref, child) => SyncLabel( + label: context.localized + .totalSize(ref.watch(syncSizeProvider(syncedItem, children))?.byteFormat ?? '--'), + status: combinedStream?.status ?? TaskStatus.notFound, + ), + ), + ), + if (combinedStream != null && combinedStream.hasDownload == true) + SyncProgressBar(item: syncedItem, task: combinedStream) + ], + ); + }, ), ), - ), - ), - Column( - children: [ - Text( - season.name, - style: Theme.of(context).textTheme.titleMedium, - ) ], ), - const Spacer(), - IconButton( - onPressed: () { - setState(() { - expanded = !expanded; - }); + trailing: SyncOptionsButton(syncedItem: syncedItem, children: children), + children: children.map( + (item) { + final baseItem = item.itemModel; + return Padding( + padding: const EdgeInsets.symmetric(vertical: 8), + child: SyncedEpisodeItem( + episode: baseItem as EpisodeModel, + syncedItem: item, + ), + ); }, - 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)), + ).toList(), + ); + }, + ), + error: (error, stackTrace) => const SizedBox.shrink(), + loading: () => const SizedBox.shrink(), ); } } 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..a0a4780 100644 --- a/lib/util/item_base_model/item_base_model_extensions.dart +++ b/lib/util/item_base_model/item_base_model_extensions.dart @@ -109,8 +109,8 @@ extension ItemBaseModelExtensions on ItemBaseModel { )) && syncAble && (canDownload ?? false); - final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this); final downloadUrl = ref.read(userProvider.notifier).createDownloadUrl(this); + final syncedItemFuture = ref.read(syncProvider.notifier).getSyncedItem(this); return [ if (!exclude.contains(ItemActions.play)) if (playAble) @@ -234,18 +234,39 @@ extension ItemBaseModelExtensions on ItemBaseModel { ), if (!exclude.contains(ItemActions.download) && downloadEnabled) ...[ if (!kIsWeb) - if (syncedItem == null) - ItemActionButton( - icon: const Icon(IconsaxPlusLinear.arrow_down_2), - label: Text(context.localized.sync), - action: () => ref.read(syncProvider.notifier).addSyncItem(context, this), - ) - else - ItemActionButton( - icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)), - action: () => showSyncItemDetails(context, syncedItem, ref), - label: Text(context.localized.syncDetails), - ) + ItemActionButton( + icon: FutureBuilder( + future: syncedItemFuture, + builder: (context, snapshot) { + final syncedItem = snapshot.data; + if (syncedItem != null) { + return IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)); + } + return const Icon(IconsaxPlusLinear.arrow_down_2); + }, + ), + label: FutureBuilder( + future: syncedItemFuture, + builder: (context, snapshot) { + final syncedItem = snapshot.data; + if (syncedItem != null) { + return Text( + context.localized.syncDetails, + ); + } + return Text(context.localized.sync); + }, + ), + action: () async { + final syncedItem = await syncedItemFuture; + if (syncedItem != null) { + await showSyncItemDetails(context, syncedItem, ref); + } else { + await ref.read(syncProvider.notifier).addSyncItem(context, this); + } + context.refreshData(); + }, + ) else if (downloadUrl != null) ...[ ItemActionButton( icon: const Icon(IconsaxPlusLinear.document_download), 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/util/migration/isar_drift_migration.dart b/lib/util/migration/isar_drift_migration.dart new file mode 100644 index 0000000..1336d44 --- /dev/null +++ b/lib/util/migration/isar_drift_migration.dart @@ -0,0 +1,82 @@ +import 'dart:convert'; +import 'dart:developer'; +import 'dart:io'; + +import 'package:flutter/foundation.dart'; + +import 'package:drift/drift.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:isar/isar.dart'; +import 'package:path/path.dart' as path; +import 'package:path_provider/path_provider.dart'; + +import 'package:fladder/models/syncing/database_item.dart'; +import 'package:fladder/models/syncing/i_synced_item.dart'; +import 'package:fladder/models/syncing/sync_item.dart'; + +Future isarMigration(Ref ref, AppDatabase db, String savePath) async { + if (kIsWeb) return; + + //Return if the database is already migrated or not empty + final isNotEmtpy = await db.select(db.databaseItems).get().then((value) => value.isNotEmpty); + if (isNotEmtpy) { + log('Isar database is not empty, skipping migration'); + return; + } + + //Open isar database + final applicationDirectory = await getApplicationDocumentsDirectory(); + final isarPath = Directory(path.joinAll([applicationDirectory.path, 'Fladder', 'Database'])); + await isarPath.create(recursive: true); + final isar = Isar.open( + schemas: [ISyncedItemSchema], + directory: isarPath.path, + ); + + //Fetch all synced items from the old database + List items = isar.iSyncedItems + .where() + .findAll() + .map((e) => SyncedItem.fromIsar(e, path.joinAll([savePath, e.userId ?? "Unkown User"]))) + .toList(); + + //Clear any missing paths + items = items.where((e) => e.path != null ? Directory(e.path!).existsSync() : false).toList(); + + //Convert to drift database items + final driftItems = items.map( + (item) => DatabaseItemsCompanion( + id: Value(item.id), + parentId: Value(item.parentId), + syncing: Value(item.syncing), + userId: Value(item.userId), + path: Value(item.path), + fileSize: Value(item.fileSize), + sortName: Value(item.sortName), + videoFileName: Value(item.videoFileName), + trickPlayModel: Value(item.fTrickPlayModel != null ? jsonEncode(item.fTrickPlayModel?.toJson()) : null), + mediaSegments: Value(item.mediaSegments != null ? jsonEncode(item.mediaSegments?.toJson()) : null), + images: Value(item.fImages != null ? jsonEncode(item.fImages?.toJson()) : null), + chapters: Value(jsonEncode(item.fChapters.map((e) => e.toJson()).toList())), + subtitles: Value(jsonEncode(item.subtitles.map((e) => e.toJson()).toList())), + userData: Value(item.userData != null ? jsonEncode(item.userData?.toJson()) : null), + ), + ); + + await db.batch((batch) { + batch.insertAll( + db.databaseItems, + driftItems, + mode: InsertMode.insertOrReplace, + ); + }); + + isar.close(); + + //Delete isar database after a few versions? + // await Future.delayed(const Duration(seconds: 1)); + // if (await isarPath.exists()) { + // log('Deleting old Fladder base folder: ${isarPath.path}'); + // await isarPath.delete(recursive: true); + // } +} diff --git a/lib/util/refresh_state.dart b/lib/util/refresh_state.dart index 97413bc..11b18e9 100644 --- a/lib/util/refresh_state.dart +++ b/lib/util/refresh_state.dart @@ -35,6 +35,8 @@ extension RefreshContextExtension on BuildContext { Future refreshData() async { //Small delay to fix server not updating response based on successful query await Future.delayed(const Duration(milliseconds: 250)); - await RefreshState.maybeOf(this)?.refresh(); + if (mounted) { + await RefreshState.maybeOf(this)?.refresh(); + } } } 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/alert_content.dart b/lib/widgets/shared/alert_content.dart index e1f3e65..8ca6eb1 100644 --- a/lib/widgets/shared/alert_content.dart +++ b/lib/widgets/shared/alert_content.dart @@ -30,7 +30,7 @@ class ActionContent extends StatelessWidget { height: 4, ), ], - Expanded(child: child), + Flexible(child: child), if (actions.isNotEmpty) ...[ if (showDividers) const Divider( diff --git a/lib/widgets/shared/icon_button_await.dart b/lib/widgets/shared/icon_button_await.dart index 2aaaa95..3972c9b 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,11 @@ class IconButtonAwaitState extends State { } catch (e) { log(e.toString()); } finally { - setState(() => loading = false); + if (mounted) { + setState(() { + loading = false; + }); + } } }, icon: AnimatedFadeSize( diff --git a/lib/widgets/shared/status_card.dart b/lib/widgets/shared/status_card.dart index 27ce87d..524a1d7 100644 --- a/lib/widgets/shared/status_card.dart +++ b/lib/widgets/shared/status_card.dart @@ -12,7 +12,7 @@ class StatusCard extends ConsumerWidget { @override Widget build(BuildContext context, WidgetRef ref) { return Padding( - padding: const EdgeInsets.all(5), + padding: const EdgeInsets.all(2), child: SizedBox.square( dimension: 33, child: Card( diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index 0485f9a..838baf4 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -39,6 +40,9 @@ void fl_register_plugins(FlPluginRegistry* registry) { g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); + g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin"); + sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar); g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 5bfb5da..7d4f5d8 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST media_kit_libs_linux media_kit_video screen_retriever_linux + sqlite3_flutter_libs url_launcher_linux volume_controller window_manager diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index bd16cf9..bebb098 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -24,6 +24,7 @@ import screen_retriever_macos import share_plus import shared_preferences_foundation import sqflite_darwin +import sqlite3_flutter_libs import url_launcher_macos import video_player_avfoundation import volume_controller @@ -51,6 +52,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) + Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) diff --git a/pubspec.lock b/pubspec.lock index 917cbff..58b08c5 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -146,10 +146,10 @@ packages: dependency: transitive description: name: build - sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 + sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7" url: "https://pub.dev" source: hosted - version: "2.4.2" + version: "2.5.4" build_cli_annotations: dependency: transitive description: @@ -178,26 +178,26 @@ packages: dependency: transitive description: name: build_resolvers - sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 + sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62 url: "https://pub.dev" source: hosted - version: "2.4.4" + version: "2.5.4" build_runner: dependency: "direct dev" description: name: build_runner - sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" + sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53" url: "https://pub.dev" source: hosted - version: "2.4.15" + version: "2.5.4" build_runner_core: dependency: transitive description: name: build_runner_core - sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" + sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792" url: "https://pub.dev" source: hosted - version: "8.0.0" + version: "9.1.2" built_collection: dependency: transitive description: @@ -430,6 +430,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.8" + db_viewer: + dependency: transitive + description: + name: db_viewer + sha256: "5f7e3cfcde9663321797d8f6f0c876f7c13f0825a2e77ec1ef065656797144d9" + url: "https://pub.dev" + source: hosted + version: "1.1.0" dbus: dependency: transitive description: @@ -438,6 +446,14 @@ packages: url: "https://pub.dev" source: hosted version: "0.7.11" + decimal: + dependency: transitive + description: + name: decimal + sha256: fc706a5618b81e5b367b01dd62621def37abc096f2b46a9bd9068b64c1fa36d0 + url: "https://pub.dev" + source: hosted + version: "3.2.4" desktop_drop: dependency: "direct main" description: @@ -454,6 +470,46 @@ packages: url: "https://pub.dev" source: hosted version: "3.0.0" + drift: + dependency: "direct main" + description: + name: drift + sha256: b584ddeb2b74436735dd2cf746d2d021e19a9a6770f409212fd5cbc2814ada85 + url: "https://pub.dev" + source: hosted + version: "2.26.1" + drift_db_viewer: + dependency: "direct main" + description: + name: drift_db_viewer + sha256: "5ea77858c52b55460a1e8f34ab5f88324621d486717d876fd745765fbc227f3f" + url: "https://pub.dev" + source: hosted + version: "2.1.0" + drift_dev: + dependency: "direct dev" + description: + name: drift_dev + sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588" + url: "https://pub.dev" + source: hosted + version: "2.26.0" + drift_flutter: + dependency: "direct main" + description: + name: drift_flutter + sha256: ccfb42bc942e59f81500b16228df59cf8eb40d2fbd96637ff677df923350af7b + url: "https://pub.dev" + source: hosted + version: "0.2.5" + drift_sync: + dependency: "direct main" + description: + name: drift_sync + sha256: "522f8e651ceb9dcfb44b576593ac2d2eae79f0663d7418fb7804b3cc4d9d27e3" + url: "https://pub.dev" + source: hosted + version: "0.13.0" dynamic_color: dependency: "direct main" description: @@ -514,10 +570,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: @@ -842,6 +898,22 @@ packages: url: "https://pub.dev" source: hosted version: "2.1.3" + google_identity_services_web: + dependency: transitive + description: + name: google_identity_services_web + sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454" + url: "https://pub.dev" + source: hosted + version: "0.3.3+1" + googleapis_auth: + dependency: transitive + description: + name: googleapis_auth + sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db + url: "https://pub.dev" + source: hosted + version: "2.0.0" graphs: dependency: transitive description: @@ -850,6 +922,14 @@ packages: url: "https://pub.dev" source: hosted version: "2.3.2" + grpc: + dependency: transitive + description: + name: grpc + sha256: "2dde469ddd8bbd7a33a0765da417abe1ad2142813efce3a86c512041294e2b26" + url: "https://pub.dev" + source: hosted + version: "4.1.0" highlight: dependency: transitive description: @@ -874,6 +954,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.4.0" + http2: + dependency: transitive + description: + name: http2 + sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa" + url: "https://pub.dev" + source: hosted + version: "2.3.1" http_client_helper: dependency: transitive description: @@ -942,18 +1030,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: @@ -1402,6 +1490,14 @@ packages: url: "https://pub.dev" source: hosted version: "6.0.2" + protobuf: + dependency: transitive + description: + name: protobuf + sha256: "6153efcc92a06910918f3db8231fd2cf828ac81e50ebd87adc8f8a8cb3caff0e" + url: "https://pub.dev" + source: hosted + version: "4.1.1" provider: dependency: transitive description: @@ -1434,6 +1530,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.7+1" + rational: + dependency: transitive + description: + name: rational + sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336 + url: "https://pub.dev" + source: hosted + version: "2.2.3" recase: dependency: transitive description: @@ -1791,6 +1895,30 @@ packages: url: "https://pub.dev" source: hosted version: "2.4.0" + sqlite3: + dependency: transitive + description: + name: sqlite3 + sha256: "608b56d594e4c8498c972c8f1507209f9fd74939971b948ddbbfbfd1c9cb3c15" + url: "https://pub.dev" + source: hosted + version: "2.7.7" + sqlite3_flutter_libs: + dependency: transitive + description: + name: sqlite3_flutter_libs + sha256: "8b4bd239bedd20ee628aed587b4c5b387328e85945c9ecbae19a93bdcd171524" + url: "https://pub.dev" + source: hosted + version: "0.5.36" + sqlparser: + dependency: transitive + description: + name: sqlparser + sha256: "7c859c803cf7e9a84d6db918bac824545045692bbe94a6386bd3a45132235d09" + url: "https://pub.dev" + source: hosted + version: "0.41.1" square_progress_indicator: dependency: "direct main" description: @@ -2027,10 +2155,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 +2267,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 +2339,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..a4a6930 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 @@ -126,6 +130,10 @@ dependencies: share_plus: ^11.0.0 archive: ^4.0.7 dart_mappable: ^4.5.0 + drift: ^2.26.1 + drift_flutter: ^0.2.5 + drift_sync: ^0.13.0 + drift_db_viewer: ^2.1.0 dev_dependencies: flutter_test: @@ -136,8 +144,9 @@ dev_dependencies: # activated in the `analysis_options.yaml` file located at the root of your # package. See that file for information about deactivating specific lint # rules and activating additional ones. + drift_dev: ^2.26.0 flutter_lints: ^6.0.0 - build_runner: ^2.4.15 + build_runner: ^2.5.4 chopper_generator: ^8.1.0 json_serializable: ^6.9.0 custom_lint: ^0.7.0 diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 803a8d9..f29af16 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -17,6 +17,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) { registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); SharePlusWindowsPluginCApiRegisterWithRegistrar( registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); + Sqlite3FlutterLibsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin")); UrlLauncherWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("UrlLauncherWindows")); VolumeControllerPluginCApiRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 80ec1f7..a0c5900 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST screen_brightness_windows screen_retriever_windows share_plus + sqlite3_flutter_libs url_launcher_windows volume_controller window_manager