mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feature: Sync 2.0 (#417)
breaking changes: Migrate from Isar to Drift database. Isar will be removed in future versions of the application the migration will only apply for as long as isar is included.
This commit is contained in:
commit
3cbcecd692
63 changed files with 3424 additions and 1285 deletions
|
|
@ -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"
|
||||
}
|
||||
|
|
@ -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<String> 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<String> 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<Main> with WindowListener, WidgetsBinding
|
|||
},
|
||||
builder: (context, child) => LocalizationContextWrapper(
|
||||
child: ScaffoldMessenger(child: child ?? Container()),
|
||||
currentLocale: language,
|
||||
),
|
||||
debugShowCheckedModeBanner: false,
|
||||
darkTheme: darkTheme.copyWith(
|
||||
|
|
|
|||
|
|
@ -26,7 +26,6 @@ class CredentialsModel {
|
|||
Map<String, String> 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}"'
|
||||
};
|
||||
|
|
|
|||
|
|
@ -45,6 +45,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
|
|||
final String? seriesName;
|
||||
final int season;
|
||||
final int episode;
|
||||
final int? episodeEnd;
|
||||
final List<Chapter> 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,
|
||||
|
|
|
|||
|
|
@ -31,6 +31,9 @@ class EpisodeModelMapper extends SubClassMapperBase<EpisodeModel> {
|
|||
static int _$episode(EpisodeModel v) => v.episode;
|
||||
static const Field<EpisodeModel, int> _f$episode =
|
||||
Field('episode', _$episode);
|
||||
static int? _$episodeEnd(EpisodeModel v) => v.episodeEnd;
|
||||
static const Field<EpisodeModel, int> _f$episodeEnd =
|
||||
Field('episodeEnd', _$episodeEnd);
|
||||
static List<Chapter> _$chapters(EpisodeModel v) => v.chapters;
|
||||
static const Field<EpisodeModel, List<Chapter>> _f$chapters =
|
||||
Field('chapters', _$chapters, opt: true, def: const []);
|
||||
|
|
@ -86,6 +89,7 @@ class EpisodeModelMapper extends SubClassMapperBase<EpisodeModel> {
|
|||
#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<EpisodeModel> {
|
|||
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<Chapter>? chapters,
|
||||
ItemLocation? location,
|
||||
DateTime? dateAired,
|
||||
|
|
@ -209,6 +215,7 @@ class _EpisodeModelCopyWithImpl<$R, $Out>
|
|||
{Object? seriesName = $none,
|
||||
int? season,
|
||||
int? episode,
|
||||
Object? episodeEnd = $none,
|
||||
List<Chapter>? 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),
|
||||
|
|
|
|||
|
|
@ -50,6 +50,21 @@ class UserData with UserDataMappable {
|
|||
|
||||
Duration get playBackPosition => Duration(milliseconds: playbackPositionTicks ~/ 10000);
|
||||
|
||||
static UserData? determineLastUserData(List<UserData?> 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<String, dynamic> map) => UserDataMapper.fromMap(map);
|
||||
factory UserData.fromJson(String json) => UserDataMapper.fromJson(json);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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<EpisodeModel>().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<EpisodeModel>().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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
193
lib/models/syncing/database_item.dart
Normal file
193
lib/models/syncing/database_item.dart
Normal file
|
|
@ -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<Column<Object>> 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<void> clearDatabase() {
|
||||
return transaction(() async {
|
||||
for (final table in allTables) {
|
||||
await delete(table).go();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Selectable<SyncedItem> getItem(String id) =>
|
||||
(select(databaseItems)..where((tbl) => tbl.id.equals(id) & tbl.userId.equals(userId))).map(databaseConverter);
|
||||
|
||||
Selectable<SyncedItem> getParent(String id) =>
|
||||
(select(databaseItems)..where((tbl) => tbl.parentId.equals(id) & tbl.userId.equals(userId)))
|
||||
.map(databaseConverter);
|
||||
|
||||
Selectable<SyncedItem> get getParentItems =>
|
||||
((select(databaseItems)..where((tbl) => (tbl.parentId.isNull() & tbl.userId.equals(userId))))
|
||||
..orderBy([(t) => OrderingTerm(expression: t.sortName)]))
|
||||
.map(databaseConverter);
|
||||
|
||||
Selectable<SyncedItem> getChildren(String parentId) =>
|
||||
((select(databaseItems)..where((tbl) => (tbl.parentId.equals(parentId) & tbl.userId.equals(userId))))
|
||||
..orderBy([(t) => OrderingTerm(expression: t.sortName)]))
|
||||
.map(databaseConverter);
|
||||
|
||||
Future<int> 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<List<SyncedItem>> 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 = <SyncedItem>[];
|
||||
List<SyncedItem> 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<void> insertMultipleEntries(List<SyncedItem> items) async {
|
||||
await batch((batch) {
|
||||
batch.insertAll(
|
||||
databaseItems,
|
||||
items.map(toDataBaseItem),
|
||||
mode: InsertMode.insertOrReplace,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> deleteAllItems(List<SyncedItem> 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/
|
||||
);
|
||||
}
|
||||
}
|
||||
1035
lib/models/syncing/database_item.g.dart
Normal file
1035
lib/models/syncing/database_item.g.dart
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -77,7 +77,16 @@ const ISyncedItemSchema = IsarGeneratedSchema(
|
|||
type: IsarType.string,
|
||||
),
|
||||
],
|
||||
indexes: [],
|
||||
indexes: [
|
||||
IsarIndexSchema(
|
||||
name: 'parentId',
|
||||
properties: [
|
||||
"parentId",
|
||||
],
|
||||
unique: false,
|
||||
hash: false,
|
||||
),
|
||||
],
|
||||
),
|
||||
converter: IsarObjectConverter<String, ISyncedItem>(
|
||||
serialize: serializeISyncedItem,
|
||||
|
|
|
|||
|
|
@ -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<Chapter> fChapters,
|
||||
@Default([]) List<SubStreamModel> 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<Chapter> 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<SyncedItem> nestedChildren(WidgetRef ref) => ref.watch(syncChildrenProvider(this));
|
||||
|
||||
List<SyncedItem> getChildren(Ref ref) => ref.read(syncProvider.notifier).getChildren(this);
|
||||
List<SyncedItem> getNestedChildren(Ref ref) => ref.read(syncProvider.notifier).getNestedChildren(this);
|
||||
Future<List<SyncedItem>> getChildren(Ref ref) async => await ref.read(syncProvider.notifier).getChildren(this);
|
||||
Future<List<SyncedItem>> getNestedChildren(Ref ref) async =>
|
||||
await ref.read(syncProvider.notifier).getNestedChildren(this);
|
||||
|
||||
Future<int> 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,
|
||||
|
|
|
|||
|
|
@ -31,7 +31,10 @@ mixin _$SyncedItem {
|
|||
List<Chapter> get fChapters => throw _privateConstructorUsedError;
|
||||
List<SubStreamModel> 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<Chapter> fChapters,
|
||||
List<SubStreamModel> 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<Chapter> fChapters,
|
||||
List<SubStreamModel> 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<Chapter> fChapters = const [],
|
||||
final List<SubStreamModel> 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<Chapter> fChapters,
|
||||
final List<SubStreamModel> 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<SubStreamModel> 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.
|
||||
|
|
|
|||
|
|
@ -72,20 +72,16 @@ class EpisodeDetailsProvider extends StateNotifier<EpisodeDetailModel> {
|
|||
}
|
||||
}
|
||||
|
||||
void _tryToCreateOfflineState(ItemBaseModel item) {
|
||||
Future<void> _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<EpisodeModel>()
|
||||
.toList();
|
||||
state = state.copyWith(
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -29,7 +29,12 @@ class SeasonDetailsNotifier extends StateNotifier<SeasonModel?> {
|
|||
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;
|
||||
|
|
|
|||
|
|
@ -32,20 +32,39 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
|
|||
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<SeriesModel?> {
|
|||
|
||||
Future<void> _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<EpisodeModel>().toList(),
|
||||
seasons: allChildren.whereType<SeasonModel>().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<EpisodeModel>().toList(),
|
||||
seasons: allChildren.whereType<SeasonModel>().toList(),
|
||||
);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<Response<QueryFilters>> itemsFilters2Get({
|
||||
|
|
@ -903,37 +909,37 @@ class JellyService {
|
|||
|
||||
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
|
||||
|
||||
Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async {
|
||||
if (account?.id == null) return null;
|
||||
Future<UserConfiguration?> _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<UserConfiguration?> updateRememberAudioSelections() {
|
||||
final currentUserConfiguration = account?.userConfiguration;
|
||||
if (currentUserConfiguration == null) return Future.value(null);
|
||||
Future<UserConfiguration?> 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<UserConfiguration?> updateRememberSubtitleSelections() {
|
||||
final current = account?.userConfiguration;
|
||||
if (current == null) return Future.value(null);
|
||||
Future<UserConfiguration?> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
? ("", "")
|
||||
: (
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@ part of 'background_download_provider.dart';
|
|||
// **************************************************************************
|
||||
|
||||
String _$backgroundDownloaderHash() =>
|
||||
r'dc27f708fc2f1695d37afcb99f8814bc024037af';
|
||||
r'9d866549ed7632e855ba30de2765368960889cff';
|
||||
|
||||
/// See also [BackgroundDownloader].
|
||||
@ProviderFor(BackgroundDownloader)
|
||||
|
|
|
|||
|
|
@ -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<SyncedItem> build(SyncedItem arg) {
|
||||
final syncedItemIsar = ref.watch(syncProvider.notifier).isar;
|
||||
final allChildren = <SyncedItem>[];
|
||||
List<SyncedItem> 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 ?? "")) ??
|
||||
<SyncedItem>[];
|
||||
},
|
||||
);
|
||||
allChildren.addAll(currentLevel.expand((list) => list));
|
||||
toProcess = currentLevel.expand((list) => list).toList();
|
||||
}
|
||||
return allChildren;
|
||||
Stream<SyncedItem?> 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<List<SyncedItem>> build(SyncedItem item) => ref.read(syncProvider.notifier).getChildren(item);
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class SyncedNestedChildren extends _$SyncedNestedChildren {
|
||||
@override
|
||||
FutureOr<List<SyncedItem>> 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<SyncedItem> 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<SyncStatus> 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<SyncedItem>? 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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<List<SyncedItem>> {
|
||||
late final SyncedItem arg;
|
||||
/// See also [syncedItem].
|
||||
@ProviderFor(syncedItem)
|
||||
const syncedItemProvider = SyncedItemFamily();
|
||||
|
||||
List<SyncedItem> build(
|
||||
SyncedItem arg,
|
||||
);
|
||||
}
|
||||
/// See also [syncedItem].
|
||||
class SyncedItemFamily extends Family<AsyncValue<SyncedItem?>> {
|
||||
/// See also [syncedItem].
|
||||
const SyncedItemFamily();
|
||||
|
||||
/// See also [SyncChildren].
|
||||
@ProviderFor(SyncChildren)
|
||||
const syncChildrenProvider = SyncChildrenFamily();
|
||||
|
||||
/// See also [SyncChildren].
|
||||
class SyncChildrenFamily extends Family<List<SyncedItem>> {
|
||||
/// 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<List<SyncedItem>> {
|
|||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncChildrenProvider';
|
||||
String? get name => r'syncedItemProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncChildren].
|
||||
class SyncChildrenProvider
|
||||
extends AutoDisposeNotifierProviderImpl<SyncChildren, List<SyncedItem>> {
|
||||
/// See also [SyncChildren].
|
||||
SyncChildrenProvider(
|
||||
SyncedItem arg,
|
||||
/// See also [syncedItem].
|
||||
class SyncedItemProvider extends AutoDisposeStreamProvider<SyncedItem?> {
|
||||
/// 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<SyncedItem> runNotifierBuild(
|
||||
covariant SyncChildren notifier,
|
||||
Override overrideWith(
|
||||
Stream<SyncedItem?> 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<SyncChildren, List<SyncedItem>>
|
||||
createElement() {
|
||||
return _SyncChildrenProviderElement(this);
|
||||
AutoDisposeStreamProviderElement<SyncedItem?> 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<List<SyncedItem>> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
mixin SyncedItemRef on AutoDisposeStreamProviderRef<SyncedItem?> {
|
||||
/// The parameter `item` of this provider.
|
||||
ItemBaseModel? get item;
|
||||
}
|
||||
|
||||
class _SyncChildrenProviderElement
|
||||
extends AutoDisposeNotifierProviderElement<SyncChildren, List<SyncedItem>>
|
||||
with SyncChildrenRef {
|
||||
_SyncChildrenProviderElement(super.provider);
|
||||
class _SyncedItemProviderElement
|
||||
extends AutoDisposeStreamProviderElement<SyncedItem?> 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<List<SyncedItem>> {
|
||||
late final SyncedItem item;
|
||||
|
||||
FutureOr<List<SyncedItem>> build(
|
||||
SyncedItem item,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [SyncedChildren].
|
||||
@ProviderFor(SyncedChildren)
|
||||
const syncedChildrenProvider = SyncedChildrenFamily();
|
||||
|
||||
/// See also [SyncedChildren].
|
||||
class SyncedChildrenFamily extends Family<AsyncValue<List<SyncedItem>>> {
|
||||
/// 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<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncedChildrenProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncedChildren].
|
||||
class SyncedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
||||
SyncedChildren, List<SyncedItem>> {
|
||||
/// 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<List<SyncedItem>> 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<SyncedChildren, List<SyncedItem>>
|
||||
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<List<SyncedItem>> {
|
||||
/// The parameter `item` of this provider.
|
||||
SyncedItem get item;
|
||||
}
|
||||
|
||||
class _SyncedChildrenProviderElement
|
||||
extends AutoDisposeAsyncNotifierProviderElement<SyncedChildren,
|
||||
List<SyncedItem>> with SyncedChildrenRef {
|
||||
_SyncedChildrenProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SyncedItem get item => (origin as SyncedChildrenProvider).item;
|
||||
}
|
||||
|
||||
String _$syncedNestedChildrenHash() =>
|
||||
r'ea8dd0e694efa6d6ec0c73d699b5fb3e933f9322';
|
||||
|
||||
abstract class _$SyncedNestedChildren
|
||||
extends BuildlessAutoDisposeAsyncNotifier<List<SyncedItem>> {
|
||||
late final SyncedItem item;
|
||||
|
||||
FutureOr<List<SyncedItem>> build(
|
||||
SyncedItem item,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [SyncedNestedChildren].
|
||||
@ProviderFor(SyncedNestedChildren)
|
||||
const syncedNestedChildrenProvider = SyncedNestedChildrenFamily();
|
||||
|
||||
/// See also [SyncedNestedChildren].
|
||||
class SyncedNestedChildrenFamily extends Family<AsyncValue<List<SyncedItem>>> {
|
||||
/// 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<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncedNestedChildrenProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncedNestedChildren].
|
||||
class SyncedNestedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl<
|
||||
SyncedNestedChildren, List<SyncedItem>> {
|
||||
/// 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<List<SyncedItem>> 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<SyncedNestedChildren,
|
||||
List<SyncedItem>> 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<List<SyncedItem>> {
|
||||
/// The parameter `item` of this provider.
|
||||
SyncedItem get item;
|
||||
}
|
||||
|
||||
class _SyncedNestedChildrenProviderElement
|
||||
extends AutoDisposeAsyncNotifierProviderElement<SyncedNestedChildren,
|
||||
List<SyncedItem>> with SyncedNestedChildrenRef {
|
||||
_SyncedNestedChildrenProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SyncedItem get item => (origin as SyncedNestedChildrenProvider).item;
|
||||
}
|
||||
|
||||
String _$syncDownloadStatusHash() =>
|
||||
r'5a0f8537a977c52e6083bd84265631ea5d160637';
|
||||
r'6ee039e094f1e007ebaeb20ae63430be829cdeb7';
|
||||
|
||||
abstract class _$SyncDownloadStatus
|
||||
extends BuildlessAutoDisposeNotifier<DownloadStream?> {
|
||||
late final SyncedItem arg;
|
||||
late final List<SyncedItem> children;
|
||||
|
||||
DownloadStream? build(
|
||||
SyncedItem arg,
|
||||
List<SyncedItem> children,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -197,9 +478,11 @@ class SyncDownloadStatusFamily extends Family<DownloadStream?> {
|
|||
/// See also [SyncDownloadStatus].
|
||||
SyncDownloadStatusProvider call(
|
||||
SyncedItem arg,
|
||||
List<SyncedItem> children,
|
||||
) {
|
||||
return SyncDownloadStatusProvider(
|
||||
arg,
|
||||
children,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -209,6 +492,7 @@ class SyncDownloadStatusFamily extends Family<DownloadStream?> {
|
|||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
provider.children,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -233,8 +517,11 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
|
|||
/// See also [SyncDownloadStatus].
|
||||
SyncDownloadStatusProvider(
|
||||
SyncedItem arg,
|
||||
List<SyncedItem> 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<SyncedItem> 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<DownloadStream?> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
|
||||
/// The parameter `children` of this provider.
|
||||
List<SyncedItem> get children;
|
||||
}
|
||||
|
||||
class _SyncDownloadStatusProviderElement
|
||||
|
|
@ -318,161 +618,20 @@ class _SyncDownloadStatusProviderElement
|
|||
|
||||
@override
|
||||
SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg;
|
||||
@override
|
||||
List<SyncedItem> get children =>
|
||||
(origin as SyncDownloadStatusProvider).children;
|
||||
}
|
||||
|
||||
String _$syncStatusesHash() => r'f05ee53368d1de130714bba09132e08aba15bc44';
|
||||
|
||||
abstract class _$SyncStatuses
|
||||
extends BuildlessAutoDisposeAsyncNotifier<SyncStatus> {
|
||||
late final SyncedItem arg;
|
||||
|
||||
FutureOr<SyncStatus> build(
|
||||
SyncedItem arg,
|
||||
);
|
||||
}
|
||||
|
||||
/// See also [SyncStatuses].
|
||||
@ProviderFor(SyncStatuses)
|
||||
const syncStatusesProvider = SyncStatusesFamily();
|
||||
|
||||
/// See also [SyncStatuses].
|
||||
class SyncStatusesFamily extends Family<AsyncValue<SyncStatus>> {
|
||||
/// 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<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'syncStatusesProvider';
|
||||
}
|
||||
|
||||
/// See also [SyncStatuses].
|
||||
class SyncStatusesProvider
|
||||
extends AutoDisposeAsyncNotifierProviderImpl<SyncStatuses, SyncStatus> {
|
||||
/// 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<SyncStatus> 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<SyncStatuses, SyncStatus>
|
||||
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<SyncStatus> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
}
|
||||
|
||||
class _SyncStatusesProviderElement
|
||||
extends AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
|
||||
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<int?> {
|
||||
late final SyncedItem arg;
|
||||
late final List<SyncedItem>? children;
|
||||
|
||||
int? build(
|
||||
SyncedItem arg,
|
||||
List<SyncedItem>? children,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -488,9 +647,11 @@ class SyncSizeFamily extends Family<int?> {
|
|||
/// See also [SyncSize].
|
||||
SyncSizeProvider call(
|
||||
SyncedItem arg,
|
||||
List<SyncedItem>? children,
|
||||
) {
|
||||
return SyncSizeProvider(
|
||||
arg,
|
||||
children,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -500,6 +661,7 @@ class SyncSizeFamily extends Family<int?> {
|
|||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
provider.children,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -523,8 +685,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
|
|||
/// See also [SyncSize].
|
||||
SyncSizeProvider(
|
||||
SyncedItem arg,
|
||||
List<SyncedItem>? 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<SyncSize, int?> {
|
|||
dependencies: SyncSizeFamily._dependencies,
|
||||
allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies,
|
||||
arg: arg,
|
||||
children: children,
|
||||
);
|
||||
|
||||
SyncSizeProvider._internal(
|
||||
|
|
@ -544,9 +710,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
|
|||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.arg,
|
||||
required this.children,
|
||||
}) : super.internal();
|
||||
|
||||
final SyncedItem arg;
|
||||
final List<SyncedItem>? children;
|
||||
|
||||
@override
|
||||
int? runNotifierBuild(
|
||||
|
|
@ -554,6 +722,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
|
|||
) {
|
||||
return notifier.build(
|
||||
arg,
|
||||
children,
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -562,13 +731,16 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
|
|||
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<SyncSize, int?> {
|
|||
|
||||
@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<SyncSize, int?> {
|
|||
mixin SyncSizeRef on AutoDisposeNotifierProviderRef<int?> {
|
||||
/// The parameter `arg` of this provider.
|
||||
SyncedItem get arg;
|
||||
|
||||
/// The parameter `children` of this provider.
|
||||
List<SyncedItem>? get children;
|
||||
}
|
||||
|
||||
class _SyncSizeProviderElement
|
||||
|
|
@ -606,6 +784,8 @@ class _SyncSizeProviderElement
|
|||
|
||||
@override
|
||||
SyncedItem get arg => (origin as SyncSizeProvider).arg;
|
||||
@override
|
||||
List<SyncedItem>? 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
|
||||
|
|
|
|||
|
|
@ -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<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError());
|
||||
|
||||
final downloadTasksProvider = StateProvider.family<DownloadStream, String>((ref, id) => DownloadStream.empty());
|
||||
|
||||
class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
||||
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<SyncSettingsModel> {
|
|||
(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<SyncSettingsModel> {
|
|||
}
|
||||
}
|
||||
|
||||
final Ref ref;
|
||||
final Isar? isar;
|
||||
final Directory mobileDirectory;
|
||||
final String subPath = "Synced";
|
||||
|
||||
StreamSubscription<List<SyncedItem>>? _subscription;
|
||||
|
||||
IsarCollection<String, ISyncedItem>? 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<SyncSettingsModel> {
|
|||
|
||||
String? get syncPath => saveDirectory?.path;
|
||||
|
||||
QueryBuilder<ISyncedItem, ISyncedItem, QAfterFilterCondition>? get getParentSyncItems =>
|
||||
syncedItems?.where().parentIdIsNull();
|
||||
|
||||
Future<int> get directorySize async {
|
||||
if (saveDirectory == null) return 0;
|
||||
var files = await saveDirectory!.list(recursive: true).toList();
|
||||
|
|
@ -167,59 +164,62 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
}
|
||||
|
||||
Future<void> 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<SyncedItem> getNestedChildren(SyncedItem item) {
|
||||
final allChildren = <SyncedItem>[];
|
||||
List<SyncedItem> 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 ?? "")) ??
|
||||
<SyncedItem>[];
|
||||
},
|
||||
);
|
||||
allChildren.addAll(currentLevel.expand((list) => list));
|
||||
toProcess = currentLevel.expand((list) => list).toList();
|
||||
}
|
||||
return allChildren;
|
||||
}
|
||||
Future<List<SyncedItem>> getNestedChildren(SyncedItem item) async => db.getNestedChildren(item);
|
||||
|
||||
List<SyncedItem> getChildren(SyncedItem item) {
|
||||
return (syncedItems?.where().parentIdEqualTo(item.id).sortBySortName().findAll())
|
||||
?.map(
|
||||
(e) => SyncedItem.fromIsar(e, syncPath ?? ""),
|
||||
)
|
||||
.toList() ??
|
||||
[];
|
||||
}
|
||||
Future<List<SyncedItem>> getChildren(SyncedItem root) async => await db.getChildren(root.id).get();
|
||||
|
||||
SyncedItem? getSyncedItem(ItemBaseModel? item) {
|
||||
Future<SyncedItem?> 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<SyncedItem?> getParentItem(String id) async => await db.getParent(id).getSingleOrNull();
|
||||
|
||||
Future<SyncedItem> refreshSyncItem(SyncedItem item) async {
|
||||
List<SyncedItem> itemsToSync = await getNestedChildren(item);
|
||||
|
||||
itemsToSync = [item, ...itemsToSync];
|
||||
|
||||
SyncedItem parentItem = item;
|
||||
|
||||
List<SyncedItem> 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<void> addSyncItem(BuildContext? context, ItemBaseModel item) async {
|
||||
|
|
@ -228,7 +228,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
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<SyncSettingsModel> {
|
|||
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<bool> 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<SyncSettingsModel> {
|
|||
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<SyncSettingsModel> {
|
|||
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<SyncSettingsModel> {
|
|||
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<SyncSettingsModel> {
|
|||
return data?.copyWith(path: fileName);
|
||||
}
|
||||
|
||||
void updateItemSync(SyncedItem syncedItem) =>
|
||||
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? "")));
|
||||
|
||||
Future<void> updateItem(SyncedItem syncedItem) async {
|
||||
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? "")));
|
||||
}
|
||||
Future<void> updateItem(SyncedItem syncedItem) async => db.insertItem(syncedItem);
|
||||
|
||||
Future<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem, DownloadTask? task) async {
|
||||
await syncedItem.deleteDatFiles(ref);
|
||||
|
|
@ -415,7 +411,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
return syncedItem;
|
||||
}
|
||||
|
||||
Future<DownloadStream?> syncVideoFile(SyncedItem syncItem, bool skipDownload) async {
|
||||
Future<DownloadStream?> syncFile(SyncedItem syncItem, bool skipDownload) async {
|
||||
cleanupTemporaryFiles();
|
||||
|
||||
final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
|
||||
|
|
@ -439,6 +435,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
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<SyncSettingsModel> {
|
|||
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<SyncSettingsModel> {
|
|||
(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<SyncSettingsModel> {
|
|||
|
||||
Future<void> 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<SyncedItem> 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<SyncedItem> _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<void> 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<SyncedItem?> 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<SyncedItem?> syncSeries(SeriesModel item, {EpisodeModel? episode}) async {
|
||||
Future<SyncedItem?> syncSeries(SeriesModel item, {SeasonModel? season, EpisodeModel? episode}) async {
|
||||
final response = await api.usersUserIdItemsItemIdGetBaseItem(
|
||||
itemId: item.id,
|
||||
);
|
||||
|
||||
List<SyncedItem> newItems = [];
|
||||
|
||||
SyncedItem? itemToDownload;
|
||||
List<SyncedItem>? 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;
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -464,7 +464,7 @@ class SplashRouteArgs {
|
|||
class SyncedRoute extends _i17.PageRouteInfo<SyncedRouteArgs> {
|
||||
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() {
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<DetailScaffold> {
|
|||
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);
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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 [],
|
||||
|
|
|
|||
|
|
@ -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<Widget> children = [
|
||||
Flexible(
|
||||
flex: 1,
|
||||
child: EpisodePoster(
|
||||
episode: episode,
|
||||
showLabel: false,
|
||||
syncedItem: syncedItem,
|
||||
actions: episode.generateActions(context, ref),
|
||||
onTap: () => episode.navigateTo(context),
|
||||
isCurrentEpisode: false,
|
||||
|
|
|
|||
|
|
@ -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<EpisodePosters> {
|
|||
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<EpisodePosters> {
|
|||
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -19,6 +19,7 @@ class PosterWidget extends ConsumerWidget {
|
|||
final int maxLines;
|
||||
final double? aspectRatio;
|
||||
final bool inlineTitle;
|
||||
final bool underTitle;
|
||||
final Set<ItemActions> excludeActions;
|
||||
final List<ItemAction> 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,
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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<ConsumerStatefulWidget> 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<SyncButton> {
|
||||
@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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<ChildSyncWidget> {
|
|||
|
||||
@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(),
|
||||
},
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<void> 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<SyncItemDetails> {
|
|||
|
||||
@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<List<SyncedItem>>(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<List<SyncedItem>>(: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),
|
||||
)
|
||||
],
|
||||
),
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SyncListItem> {
|
|||
@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<SyncListItem> {
|
|||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<SyncedItem> 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<SeasonModel>().length;
|
||||
final itemBaseModels = children.map((e) => e.itemModel);
|
||||
final episodes = itemBaseModels.whereType<EpisodeModel>().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<SeasonModel>().length;
|
||||
final episodes = itemBaseModels.whereType<EpisodeModel>().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)),
|
||||
},
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -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<SyncedScreen> {
|
|||
)
|
||||
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(
|
||||
|
|
|
|||
200
lib/screens/syncing/widgets/sync_options_button.dart
Normal file
200
lib/screens/syncing/widgets/sync_options_button.dart
Normal file
|
|
@ -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<SyncedItem> 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 <PopupMenuEntry>[
|
||||
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<dynamic> _deleteSyncedItems(
|
||||
BuildContext context, SyncedItem syncedItem, List<SyncedItem> 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<dynamic> _syncRemainingItems(
|
||||
BuildContext context, SyncedItem syncedItem, List<SyncedItem> 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,
|
||||
),
|
||||
)
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
|
@ -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<SyncedItem> 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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SyncedEpisodeItem> createState() => _SyncedEpisodeItemState();
|
||||
|
|
@ -38,87 +39,94 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
|
|||
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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<SyncedSeasonPoster> {
|
|||
@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: <Widget>[
|
||||
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(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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<BuildContext?>((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<LocalizationContextWrapper> createState() => _LocalizationContextWrapperState();
|
||||
|
|
@ -23,8 +29,21 @@ class _LocalizationContextWrapperState extends ConsumerState<LocalizationContext
|
|||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
WidgetsBinding.instance.addPostFrameCallback((event) {
|
||||
updateLanguageContext();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(covariant LocalizationContextWrapper oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.currentLocale != widget.currentLocale) {
|
||||
updateLanguageContext();
|
||||
}
|
||||
}
|
||||
|
||||
void updateLanguageContext() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((value) {
|
||||
ref.read(localizationContextProvider.notifier).update((cb) => context);
|
||||
ref.read(backgroundDownloaderProvider.notifier).updateTranslations(context);
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
82
lib/util/migration/isar_drift_migration.dart
Normal file
82
lib/util/migration/isar_drift_migration.dart
Normal file
|
|
@ -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<void> 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<SyncedItem> 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);
|
||||
// }
|
||||
}
|
||||
|
|
@ -35,6 +35,8 @@ extension RefreshContextExtension on BuildContext {
|
|||
Future<void> 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,8 +59,9 @@ class _BackgroundImageState extends ConsumerState<BackgroundImage> {
|
|||
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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ class ActionContent extends StatelessWidget {
|
|||
height: 4,
|
||||
),
|
||||
],
|
||||
Expanded(child: child),
|
||||
Flexible(child: child),
|
||||
if (actions.isNotEmpty) ...[
|
||||
if (showDividers)
|
||||
const Divider(
|
||||
|
|
|
|||
|
|
@ -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<dynamic> Function() onPressed;
|
||||
final Color? color;
|
||||
|
|
@ -33,7 +34,11 @@ class IconButtonAwaitState extends State<IconButtonAwait> {
|
|||
} catch (e) {
|
||||
log(e.toString());
|
||||
} finally {
|
||||
setState(() => loading = false);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
loading = false;
|
||||
});
|
||||
}
|
||||
}
|
||||
},
|
||||
icon: AnimatedFadeSize(
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue