mirror of
https://github.com/gabehf/Fladder.git
synced 2026-03-07 21:48:14 -08:00
feature: Re-implemented syncing
This commit is contained in:
parent
c5c7f71b84
commit
86ff355e21
51 changed files with 3067 additions and 1147 deletions
|
|
@ -1242,5 +1242,47 @@
|
|||
"notificationDownloadingDownloading": "Downloading",
|
||||
"notificationDownloadingPaused": "Download paused",
|
||||
"notificationDownloadingFinished": "Download finished",
|
||||
"notificationDownloadingError": "Download error"
|
||||
"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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,6 @@ 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';
|
||||
|
|
@ -20,7 +19,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';
|
||||
|
|
@ -100,12 +98,6 @@ void main(List<String> args) async {
|
|||
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args)),
|
||||
syncProvider.overrideWith((ref) => SyncNotifier(
|
||||
ref,
|
||||
!kIsWeb
|
||||
? Isar.open(
|
||||
schemas: [ISyncedItemSchema],
|
||||
directory: isarPath.path,
|
||||
)
|
||||
: null,
|
||||
applicationDirectory,
|
||||
))
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ class SeasonModel extends ItemBaseModel with SeasonModelMappable {
|
|||
}
|
||||
|
||||
@override
|
||||
bool get syncAble => true;
|
||||
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};
|
||||
}
|
||||
|
||||
@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,24 +6,6 @@ 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;
|
||||
|
|
|
|||
|
|
@ -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,6 +43,8 @@ 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";
|
||||
|
|
@ -70,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;
|
||||
|
|
@ -103,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();
|
||||
|
|
@ -158,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;
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
|
|||
ItemFields.mediasources,
|
||||
ItemFields.overview,
|
||||
ItemFields.candownload,
|
||||
ItemFields.childcount,
|
||||
]);
|
||||
|
||||
final newEpisodes = EpisodeModel.episodesFromDto(
|
||||
|
|
@ -45,10 +46,19 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
|
|||
ref,
|
||||
);
|
||||
final episodesCanDownload = newEpisodes.any((episode) => episode.canDownload == true);
|
||||
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id);
|
||||
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id, fields: [
|
||||
ItemFields.mediastreams,
|
||||
ItemFields.mediasources,
|
||||
ItemFields.overview,
|
||||
ItemFields.candownload,
|
||||
ItemFields.childcount,
|
||||
]);
|
||||
newState = newState.copyWith(
|
||||
seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref)
|
||||
.map((element) => element.copyWith(canDownload: true))
|
||||
.map((element) => element.copyWith(
|
||||
canDownload: true,
|
||||
episodes: newEpisodes.where((episode) => episode.season == element.season).toList(),
|
||||
))
|
||||
.toList(),
|
||||
);
|
||||
|
||||
|
|
@ -68,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,43 +1,34 @@
|
|||
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 root) {
|
||||
final isar = ref.watch(syncProvider.notifier).isar;
|
||||
final syncPath = ref.read(syncProvider.notifier).syncPath ?? "";
|
||||
|
||||
if (isar == null) return [];
|
||||
|
||||
final all = <SyncedItem>[];
|
||||
List<SyncedItem> toProcess = [root];
|
||||
|
||||
while (toProcess.isNotEmpty) {
|
||||
final parentIds = toProcess.map((e) => e.id).toList();
|
||||
|
||||
final children = <ISyncedItem>[];
|
||||
for (final id in parentIds) {
|
||||
final results = isar.iSyncedItems.where().parentIdEqualTo(id).sortBySortName().findAll();
|
||||
children.addAll(results);
|
||||
}
|
||||
|
||||
if (children.isEmpty) break;
|
||||
|
||||
final wrapped = children.map((e) => SyncedItem.fromIsar(e, syncPath)).toList();
|
||||
all.addAll(wrapped);
|
||||
toProcess = wrapped;
|
||||
}
|
||||
|
||||
return all;
|
||||
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
|
||||
|
|
@ -55,49 +46,33 @@ 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, List<SyncedItem>? children) async {
|
||||
final nestedChildren = children;
|
||||
|
||||
ref.watch(downloadTasksProvider(arg.id));
|
||||
if (nestedChildren != null) {
|
||||
for (var element in nestedChildren) {
|
||||
ref.watch(downloadTasksProvider(element.id));
|
||||
}
|
||||
|
||||
for (var i = 0; i < nestedChildren.length; i++) {
|
||||
final item = nestedChildren[i];
|
||||
if (item.hasVideoFile && !await item.videoFile.exists()) {
|
||||
return SyncStatus.partially;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (arg.hasVideoFile && !await arg.videoFile.exists()) {
|
||||
return SyncStatus.partially;
|
||||
}
|
||||
return SyncStatus.complete;
|
||||
}
|
||||
}
|
||||
|
||||
@riverpod
|
||||
class SyncSize extends _$SyncSize {
|
||||
@override
|
||||
|
|
@ -112,7 +87,9 @@ class SyncSize extends _$SyncSize {
|
|||
ref.watch(downloadTasksProvider(element.id));
|
||||
}
|
||||
for (var element in nestedChildren) {
|
||||
size += element.fileSize ?? 0;
|
||||
if (element.videoFile.existsSync()) {
|
||||
size += element.fileSize ?? 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ part of 'sync_provider_helpers.dart';
|
|||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$syncChildrenHash() => r'c5a90d630d49f59ad4fbaacb5154f1205799f5ab';
|
||||
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 root;
|
||||
/// See also [syncedItem].
|
||||
@ProviderFor(syncedItem)
|
||||
const syncedItemProvider = SyncedItemFamily();
|
||||
|
||||
List<SyncedItem> build(
|
||||
SyncedItem root,
|
||||
);
|
||||
}
|
||||
/// 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 root,
|
||||
/// See also [syncedItem].
|
||||
SyncedItemProvider call(
|
||||
ItemBaseModel? item,
|
||||
) {
|
||||
return SyncChildrenProvider(
|
||||
root,
|
||||
return SyncedItemProvider(
|
||||
item,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SyncChildrenProvider getProviderOverride(
|
||||
covariant SyncChildrenProvider provider,
|
||||
SyncedItemProvider getProviderOverride(
|
||||
covariant SyncedItemProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.root,
|
||||
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 root,
|
||||
/// See also [syncedItem].
|
||||
class SyncedItemProvider extends AutoDisposeStreamProvider<SyncedItem?> {
|
||||
/// See also [syncedItem].
|
||||
SyncedItemProvider(
|
||||
ItemBaseModel? item,
|
||||
) : this._internal(
|
||||
() => SyncChildren()..root = root,
|
||||
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,
|
||||
root: root,
|
||||
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.root,
|
||||
required this.item,
|
||||
}) : super.internal();
|
||||
|
||||
final SyncedItem root;
|
||||
final ItemBaseModel? item;
|
||||
|
||||
@override
|
||||
List<SyncedItem> runNotifierBuild(
|
||||
covariant SyncChildren notifier,
|
||||
Override overrideWith(
|
||||
Stream<SyncedItem?> Function(SyncedItemRef provider) create,
|
||||
) {
|
||||
return notifier.build(
|
||||
root,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(SyncChildren Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SyncChildrenProvider._internal(
|
||||
() => create()..root = root,
|
||||
override: SyncedItemProvider._internal(
|
||||
(ref) => create(ref as SyncedItemRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
root: root,
|
||||
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.root == root;
|
||||
return other is SyncedItemProvider && other.item == item;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, root.hashCode);
|
||||
hash = _SystemHash.combine(hash, item.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
|
|
@ -159,22 +144,316 @@ class SyncChildrenProvider
|
|||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin SyncChildrenRef on AutoDisposeNotifierProviderRef<List<SyncedItem>> {
|
||||
/// The parameter `root` of this provider.
|
||||
SyncedItem get root;
|
||||
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 root => (origin as SyncChildrenProvider).root;
|
||||
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'1036352200e1138b4ef70e524c0baf13bb9cd452';
|
||||
r'6ee039e094f1e007ebaeb20ae63430be829cdeb7';
|
||||
|
||||
abstract class _$SyncDownloadStatus
|
||||
extends BuildlessAutoDisposeNotifier<DownloadStream?> {
|
||||
|
|
@ -344,176 +623,7 @@ class _SyncDownloadStatusProviderElement
|
|||
(origin as SyncDownloadStatusProvider).children;
|
||||
}
|
||||
|
||||
String _$syncStatusesHash() => r'64a3499fc7b7bbdbd6594b1eec76cf42a119a041';
|
||||
|
||||
abstract class _$SyncStatuses
|
||||
extends BuildlessAutoDisposeAsyncNotifier<SyncStatus> {
|
||||
late final SyncedItem arg;
|
||||
late final List<SyncedItem>? children;
|
||||
|
||||
FutureOr<SyncStatus> build(
|
||||
SyncedItem arg,
|
||||
List<SyncedItem>? children,
|
||||
);
|
||||
}
|
||||
|
||||
/// 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,
|
||||
List<SyncedItem>? children,
|
||||
) {
|
||||
return SyncStatusesProvider(
|
||||
arg,
|
||||
children,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
SyncStatusesProvider getProviderOverride(
|
||||
covariant SyncStatusesProvider provider,
|
||||
) {
|
||||
return call(
|
||||
provider.arg,
|
||||
provider.children,
|
||||
);
|
||||
}
|
||||
|
||||
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,
|
||||
List<SyncedItem>? children,
|
||||
) : this._internal(
|
||||
() => SyncStatuses()
|
||||
..arg = arg
|
||||
..children = children,
|
||||
from: syncStatusesProvider,
|
||||
name: r'syncStatusesProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$syncStatusesHash,
|
||||
dependencies: SyncStatusesFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
SyncStatusesFamily._allTransitiveDependencies,
|
||||
arg: arg,
|
||||
children: children,
|
||||
);
|
||||
|
||||
SyncStatusesProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.arg,
|
||||
required this.children,
|
||||
}) : super.internal();
|
||||
|
||||
final SyncedItem arg;
|
||||
final List<SyncedItem>? children;
|
||||
|
||||
@override
|
||||
FutureOr<SyncStatus> runNotifierBuild(
|
||||
covariant SyncStatuses notifier,
|
||||
) {
|
||||
return notifier.build(
|
||||
arg,
|
||||
children,
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
Override overrideWith(SyncStatuses Function() create) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: SyncStatusesProvider._internal(
|
||||
() => create()
|
||||
..arg = arg
|
||||
..children = children,
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
arg: arg,
|
||||
children: children,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
|
||||
createElement() {
|
||||
return _SyncStatusesProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is SyncStatusesProvider &&
|
||||
other.arg == arg &&
|
||||
other.children == children;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, arg.hashCode);
|
||||
hash = _SystemHash.combine(hash, children.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@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;
|
||||
|
||||
/// The parameter `children` of this provider.
|
||||
List<SyncedItem>? get children;
|
||||
}
|
||||
|
||||
class _SyncStatusesProviderElement
|
||||
extends AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
|
||||
with SyncStatusesRef {
|
||||
_SyncStatusesProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
SyncedItem get arg => (origin as SyncStatusesProvider).arg;
|
||||
@override
|
||||
List<SyncedItem>? get children => (origin as SyncStatusesProvider).children;
|
||||
}
|
||||
|
||||
String _$syncSizeHash() => r'81797ecc4a6f600691b6f1fe0c16bae0228ec920';
|
||||
String _$syncSizeHash() => r'eeb6ab8dc1fdf5696c06e53f04a0e54ad68c6748';
|
||||
|
||||
abstract class _$SyncSize extends BuildlessAutoDisposeNotifier<int?> {
|
||||
late final SyncedItem arg;
|
||||
|
|
|
|||
|
|
@ -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,13 +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';
|
||||
|
|
@ -38,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(
|
||||
|
|
@ -55,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);
|
||||
});
|
||||
}
|
||||
|
|
@ -117,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)
|
||||
|
|
@ -151,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();
|
||||
|
|
@ -168,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 {
|
||||
|
|
@ -229,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;
|
||||
}
|
||||
|
|
@ -244,18 +243,24 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
|
|||
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
|
||||
|
|
@ -268,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];
|
||||
|
|
@ -396,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);
|
||||
|
|
@ -512,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: []);
|
||||
}
|
||||
|
||||
|
|
@ -526,10 +520,24 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async {
|
||||
final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref);
|
||||
|
||||
final existingSyncedItem = getSyncedItem(item);
|
||||
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]));
|
||||
|
|
@ -550,16 +558,7 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
path: directory.path,
|
||||
userData: item.userData,
|
||||
);
|
||||
|
||||
if (parent == null) {
|
||||
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
|
||||
}
|
||||
|
||||
return syncItem.copyWith(
|
||||
fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
|
||||
syncing: false,
|
||||
videoFileName: response.path?.split('/').lastOrNull ?? "",
|
||||
);
|
||||
return syncItem;
|
||||
}
|
||||
|
||||
Future<SyncedItem?> syncMovie(ItemBaseModel item, {bool skipDownload = false}) async {
|
||||
|
|
@ -574,9 +573,9 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
|
||||
if (!syncItem.directory.existsSync()) return null;
|
||||
|
||||
await syncFile(syncItem, skipDownload);
|
||||
await db.insertItem(syncItem);
|
||||
|
||||
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
|
||||
await syncFile(syncItem, skipDownload);
|
||||
|
||||
return syncItem;
|
||||
}
|
||||
|
|
@ -595,6 +594,7 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
if (!seriesItem.directory.existsSync()) return null;
|
||||
|
||||
final seasonsResponse = await api.showsSeriesIdSeasonsGet(
|
||||
seriesId: item.id,
|
||||
isMissing: false,
|
||||
enableUserData: true,
|
||||
fields: [
|
||||
|
|
@ -615,7 +615,6 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
ItemFields.chapters,
|
||||
ItemFields.trickplay,
|
||||
],
|
||||
seriesId: item.id,
|
||||
);
|
||||
|
||||
final seasons = seasonsResponse.body?.items ?? [];
|
||||
|
|
@ -660,23 +659,17 @@ extension SyncNotifierHelpers on SyncNotifier {
|
|||
|
||||
for (final (ep, newEpisode) in episodeResults) {
|
||||
newItems.add(newEpisode);
|
||||
if (episode?.id == ep.id || newSeason.id == season?.id) {
|
||||
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);
|
||||
|
||||
for (var i = 0; i < itemsToDownload.length; i++) {
|
||||
final item = itemsToDownload[i];
|
||||
await syncFile(item, false);
|
||||
syncFile(item, false);
|
||||
}
|
||||
|
||||
return seriesItem;
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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, null)).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,7 @@ import 'package:flutter/material.dart';
|
|||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/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';
|
||||
|
|
@ -58,7 +58,6 @@ class SeasonPoster extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(season);
|
||||
Padding placeHolder(String title) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.all(4),
|
||||
|
|
@ -100,19 +99,25 @@ class SeasonPoster extends ConsumerWidget {
|
|||
alignment: Alignment.topLeft,
|
||||
child: placeHolder(season.name),
|
||||
),
|
||||
if (season.userData.unPlayedItemCount != 0)
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.end,
|
||||
children: [
|
||||
if (syncedItem != null)
|
||||
StatusCard(
|
||||
child: SyncButton(
|
||||
item: season,
|
||||
syncedItem: syncedItem,
|
||||
),
|
||||
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,
|
||||
|
|
@ -122,20 +127,20 @@ class SeasonPoster extends ConsumerWidget {
|
|||
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(
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
@ -7,48 +8,44 @@ 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';
|
||||
|
||||
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, null)).value : null;
|
||||
final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem, [])) : null;
|
||||
return Stack(
|
||||
alignment: Alignment.center,
|
||||
children: [
|
||||
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,
|
||||
),
|
||||
),
|
||||
)
|
||||
],
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,7 +5,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
|
|||
import 'package:fladder/models/items/episode_model.dart';
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
import 'package:fladder/models/syncing/sync_item.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/screens/syncing/widgets/synced_season_poster.dart';
|
||||
|
||||
import 'widgets/synced_episode_item.dart';
|
||||
|
|
@ -26,8 +25,7 @@ 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();
|
||||
}
|
||||
|
|
@ -47,7 +45,6 @@ class _ChildSyncWidgetState extends ConsumerState<ChildSyncWidget> {
|
|||
EpisodeModel episode => SyncedEpisodeItem(
|
||||
episode: episode,
|
||||
syncedItem: syncedItem,
|
||||
hasFile: hasFile,
|
||||
),
|
||||
_ => Container(),
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,9 +1,9 @@
|
|||
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/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';
|
||||
|
|
@ -13,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 {
|
||||
|
|
@ -48,166 +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,
|
||||
children: syncChildren,
|
||||
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, syncChildren)).byteFormat ?? '--'),
|
||||
status: ref.watch(syncStatusesProvider(syncedItem, syncChildren)).value ??
|
||||
SyncStatus.partially,
|
||||
),
|
||||
if (combinedStream?.task != null && combinedStream != null) ...{
|
||||
SyncProgressBar(item: syncedItem, task: combinedStream)
|
||||
},
|
||||
].addInBetween(const SizedBox(height: 8)),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile)
|
||||
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),
|
||||
),
|
||||
].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,8 +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 children = ref.watch(syncChildrenProvider(syncedItem));
|
||||
final baseItem = syncedItem.itemModel;
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: SyncStatusOverlay(
|
||||
|
|
@ -67,87 +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,
|
||||
children: children,
|
||||
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,
|
||||
children: children,
|
||||
),
|
||||
),
|
||||
Flexible(
|
||||
child: SyncLabel(
|
||||
label: context.localized.totalSize(
|
||||
ref.watch(syncSizeProvider(syncedItem, children)).byteFormat ?? '--'),
|
||||
status: ref.watch(syncStatusesProvider(syncedItem, children)).value ??
|
||||
SyncStatus.partially,
|
||||
),
|
||||
),
|
||||
if (combinedStream != null && combinedStream.hasDownload == true)
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
|
|||
|
|
@ -12,7 +12,6 @@ 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 = {
|
||||
|
|
@ -24,23 +23,23 @@ const _cancellableStatuses = {
|
|||
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
|
@ -68,6 +67,7 @@ class SyncProgressBar extends ConsumerWidget {
|
|||
Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
spacing: 8,
|
||||
children: [
|
||||
Flexible(
|
||||
child: LinearProgressIndicator(
|
||||
|
|
@ -79,7 +79,7 @@ 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),
|
||||
|
|
@ -101,7 +101,7 @@ class SyncProgressBar extends ConsumerWidget {
|
|||
),
|
||||
],
|
||||
},
|
||||
].addInBetween(const SizedBox(width: 8)),
|
||||
],
|
||||
),
|
||||
const SizedBox(width: 6),
|
||||
],
|
||||
|
|
@ -120,33 +120,46 @@ class SyncSubtitle extends ConsumerWidget {
|
|||
|
||||
@override
|
||||
Widget build(BuildContext context, WidgetRef ref) {
|
||||
final baseItem = ref.read(syncProvider.notifier).getItem(syncItem);
|
||||
final syncStatus = ref.watch(syncStatusesProvider(syncItem, children)).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(
|
||||
|
|
|
|||
139
lib/screens/syncing/widgets/sync_options_button.dart
Normal file
139
lib/screens/syncing/widgets/sync_options_button.dart
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
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/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();
|
||||
return [
|
||||
PopupMenuItem(
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(IconsaxPlusLinear.refresh_2),
|
||||
Text(context.localized.refreshMetadata),
|
||||
],
|
||||
),
|
||||
onTap: () => context.refreshData(),
|
||||
),
|
||||
if (children.isNotEmpty) ...[
|
||||
PopupMenuItem(
|
||||
enabled: unSyncedChildren.isNotEmpty,
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(IconsaxPlusLinear.cloud_add),
|
||||
Text(context.localized.sync),
|
||||
],
|
||||
),
|
||||
onTap: () async => _syncRemainingItems(context, syncedItem, unSyncedChildren, ref),
|
||||
),
|
||||
PopupMenuItem(
|
||||
enabled: syncedChildren.isNotEmpty,
|
||||
child: Row(
|
||||
spacing: 12,
|
||||
children: [
|
||||
const Icon(IconsaxPlusLinear.trash),
|
||||
Text(context.localized.delete),
|
||||
],
|
||||
),
|
||||
onTap: () async => _deleteSyncedItems(context, syncedItem, syncedChildren, ref),
|
||||
)
|
||||
]
|
||||
];
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
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,6 +1,7 @@
|
|||
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:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
|
|
@ -22,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();
|
||||
|
|
@ -40,91 +39,94 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
|
|||
final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id));
|
||||
final hasFile = widget.syncedItem.videoFile.existsSync();
|
||||
|
||||
return Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3),
|
||||
child: FlatButton(
|
||||
onTap: () {
|
||||
widget.episode.navigateTo(context);
|
||||
return context.maybePop();
|
||||
},
|
||||
child: SizedBox(
|
||||
width: 175,
|
||||
child: EpisodePoster(
|
||||
episode: widget.episode,
|
||||
syncedItem: syncedItem,
|
||||
actions: [],
|
||||
showLabel: false,
|
||||
isCurrentEpisode: false,
|
||||
return IntrinsicHeight(
|
||||
child: Row(
|
||||
children: [
|
||||
ConstrainedBox(
|
||||
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3),
|
||||
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).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)),
|
||||
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,17 +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:iconsax_plus/iconsax_plus.dart';
|
||||
|
||||
import 'package:fladder/models/items/episode_model.dart';
|
||||
import 'package:fladder/models/items/season_model.dart';
|
||||
import 'package:fladder/models/syncing/sync_item.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/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/widgets/shared/icon_button_await.dart';
|
||||
import 'package:fladder/util/localization_helper.dart';
|
||||
import 'package:fladder/util/size_formatting.dart';
|
||||
|
||||
class SyncedSeasonPoster extends ConsumerStatefulWidget {
|
||||
const SyncedSeasonPoster({
|
||||
|
|
@ -32,72 +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);
|
||||
final unSyncedChildren = children.where((child) => child.status == SyncStatus.partially).toList();
|
||||
return ExpansionTile(
|
||||
tilePadding: EdgeInsets.zero,
|
||||
title: Row(
|
||||
spacing: 6,
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 75,
|
||||
child: AspectRatio(
|
||||
aspectRatio: 0.65,
|
||||
child: FlatButton(
|
||||
onTap: () {
|
||||
season.navigateTo(context);
|
||||
return context.maybePop();
|
||||
},
|
||||
child: Card(
|
||||
child: FladderImage(
|
||||
image: season.getPosters?.primary ??
|
||||
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,
|
||||
)
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
trailing: Row(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
if (unSyncedChildren.isNotEmpty)
|
||||
IconButtonAwait(
|
||||
onPressed: () async {
|
||||
for (var i = 0; i < unSyncedChildren.length; i++) {
|
||||
final childSyncedItem = unSyncedChildren[i];
|
||||
await ref.read(syncProvider.notifier).syncFile(childSyncedItem, false);
|
||||
}
|
||||
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: const Icon(IconsaxPlusLinear.cloud_change),
|
||||
),
|
||||
],
|
||||
),
|
||||
children: children.map(
|
||||
(item) {
|
||||
final baseItem = ref.read(syncProvider.notifier).getItem(item);
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8),
|
||||
child: IntrinsicHeight(
|
||||
child: SyncedEpisodeItem(
|
||||
episode: baseItem as EpisodeModel,
|
||||
syncedItem: item,
|
||||
hasFile: item.videoFile.existsSync(),
|
||||
),
|
||||
),
|
||||
).toList(),
|
||||
);
|
||||
},
|
||||
).toList(),
|
||||
),
|
||||
error: (error, stackTrace) => const SizedBox.shrink(),
|
||||
loading: () => const SizedBox.shrink(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,6 @@ import 'package:fladder/models/item_base_model.dart';
|
|||
import 'package:fladder/models/items/episode_model.dart';
|
||||
import 'package:fladder/models/items/item_shared_models.dart';
|
||||
import 'package:fladder/models/items/photos_model.dart';
|
||||
import 'package:fladder/models/syncing/sync_item.dart';
|
||||
import 'package:fladder/providers/sync_provider.dart';
|
||||
import 'package:fladder/providers/user_provider.dart';
|
||||
import 'package:fladder/screens/collections/add_to_collection.dart';
|
||||
|
|
@ -110,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)
|
||||
|
|
@ -235,22 +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: () => syncedItem.status == SyncStatus.complete
|
||||
? ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, null)
|
||||
: ref.read(syncProvider.notifier).syncFile(syncedItem, false),
|
||||
label: Text(
|
||||
syncedItem.status == SyncStatus.complete ? context.localized.delete : context.localized.sync,
|
||||
),
|
||||
)
|
||||
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),
|
||||
|
|
|
|||
80
lib/util/migration/isar_drift_migration.dart
Normal file
80
lib/util/migration/isar_drift_migration.dart
Normal file
|
|
@ -0,0 +1,80 @@
|
|||
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
|
||||
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,
|
||||
);
|
||||
});
|
||||
|
||||
//Delete database file
|
||||
final baseFolder = Directory(path.join(applicationDirectory.path, 'Fladder'));
|
||||
if (await baseFolder.exists()) {
|
||||
log('Deleting old Fladder base folder: ${baseFolder.path}');
|
||||
// await baseFolder.delete(recursive: true);
|
||||
}
|
||||
}
|
||||
|
|
@ -30,7 +30,7 @@ class ActionContent extends StatelessWidget {
|
|||
height: 4,
|
||||
),
|
||||
],
|
||||
Expanded(child: child),
|
||||
Flexible(child: child),
|
||||
if (actions.isNotEmpty) ...[
|
||||
if (showDividers)
|
||||
const Divider(
|
||||
|
|
|
|||
|
|
@ -34,10 +34,11 @@ class IconButtonAwaitState extends State<IconButtonAwait> {
|
|||
} catch (e) {
|
||||
log(e.toString());
|
||||
} finally {
|
||||
setState(() {
|
||||
if (!mounted) return;
|
||||
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