feature: Sync 2.0 (#417)

breaking changes: Migrate from Isar to Drift database. Isar will be removed in future versions of the application the migration will only apply for as long as isar is included.
This commit is contained in:
PartyDonut 2025-07-27 13:14:37 +02:00 committed by GitHub
commit 3cbcecd692
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
63 changed files with 3424 additions and 1285 deletions

2
.vscode/launch.json vendored
View file

@ -81,7 +81,6 @@
"args": [ "args": [
"--web-port", "--web-port",
"9090", "9090",
"--web-experimental-hot-reload"
], ],
}, },
{ {
@ -93,7 +92,6 @@
"args": [ "args": [
"--web-port", "--web-port",
"9090", "9090",
"--web-experimental-hot-reload"
], ],
}, },
], ],

View file

@ -1238,5 +1238,56 @@
}, },
"newUpdateFoundOnGithub": "Found a new update on Github", "newUpdateFoundOnGithub": "Found a new update on Github",
"enableBackgroundPostersTitle": "Enable background posters", "enableBackgroundPostersTitle": "Enable background posters",
"enableBackgroundPostersDesc": "Show random posters in applicable screens" "enableBackgroundPostersDesc": "Show random posters in applicable screens",
"notificationDownloadingDownloading": "Downloading",
"notificationDownloadingPaused": "Download paused",
"notificationDownloadingFinished": "Download finished",
"notificationDownloadingError": "Download error",
"syncAllItemsTitle": "Sync all items from {itemName}?",
"@syncAllItemsTitle": {
"description": "syncAllItemsFrom",
"placeholders": {
"itemName": {
"type": "String"
}
}
},
"syncAllItemsDesc": "This will sync ({itemCount}) items from '{itemName}' to your device.\nThis can take a while depending on the amount of items.",
"@syncAllItemsDesc": {
"description": "syncAllitemsFromDesc",
"placeholders": {
"itemName": {
"type": "String"
},
"itemCount": {
"type": "int"
}
}
},
"syncDeleteAllItemsTitle": "Delete all synced items from {itemName}?",
"@syncDeleteAllItemsTitle": {
"description": "syncDeleteAllitemsFrom",
"placeholders": {
"itemName": {
"type": "String"
}
}
},
"syncDeleteAllItemsDesc": "This will delete all synced items from '{itemName}'.\nThis is permanent and you will need to re-sync ({itemCount}) files.",
"@syncDeleteAllItemsDesc": {
"description": "syncDeleteAllitemsFromDesc",
"placeholders": {
"itemName": {
"type": "String"
},
"itemCount": {
"type": "int"
}
}
},
"syncPauseAll": "Pause all",
"syncResumeAll": "Resume all",
"syncStopAll": "Stop all",
"syncDeleteAll": "Delete all files",
"syncAllFiles": "Sync all files"
} }

View file

@ -8,9 +8,7 @@ import 'package:flutter/services.dart';
import 'package:dynamic_color/dynamic_color.dart'; import 'package:dynamic_color/dynamic_color.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:package_info_plus/package_info_plus.dart'; import 'package:package_info_plus/package_info_plus.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
import 'package:shared_preferences/shared_preferences.dart'; import 'package:shared_preferences/shared_preferences.dart';
import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:fladder/stubs/web/smtc_web.dart'; import 'package:smtc_windows/smtc_windows.dart' if (dart.library.html) 'package:fladder/stubs/web/smtc_web.dart';
@ -20,7 +18,6 @@ import 'package:window_manager/window_manager.dart';
import 'package:fladder/l10n/generated/app_localizations.dart'; import 'package:fladder/l10n/generated/app_localizations.dart';
import 'package:fladder/models/account_model.dart'; import 'package:fladder/models/account_model.dart';
import 'package:fladder/models/settings/arguments_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/arguments_provider.dart';
import 'package:fladder/providers/crash_log_provider.dart'; import 'package:fladder/providers/crash_log_provider.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
@ -71,13 +68,10 @@ void main(List<String> args) async {
PackageInfo packageInfo = await PackageInfo.fromPlatform(); PackageInfo packageInfo = await PackageInfo.fromPlatform();
Directory isarPath = Directory("");
Directory applicationDirectory = Directory(""); Directory applicationDirectory = Directory("");
if (!kIsWeb) { if (!kIsWeb) {
applicationDirectory = await getApplicationDocumentsDirectory(); applicationDirectory = await getApplicationDocumentsDirectory();
isarPath = Directory(path.joinAll([applicationDirectory.path, 'Fladder', 'Database']));
await isarPath.create(recursive: true);
} }
if (_isDesktop) { if (_isDesktop) {
@ -98,16 +92,7 @@ void main(List<String> args) async {
applicationInfoProvider.overrideWith((ref) => applicationInfo), applicationInfoProvider.overrideWith((ref) => applicationInfo),
crashLogProvider.overrideWith((ref) => crashProvider), crashLogProvider.overrideWith((ref) => crashProvider),
argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args)), argumentsStateProvider.overrideWith((ref) => ArgumentsModel.fromArguments(args)),
syncProvider.overrideWith((ref) => SyncNotifier( syncProvider.overrideWith((ref) => SyncNotifier(ref, applicationDirectory))
ref,
!kIsWeb
? Isar.open(
schemas: [ISyncedItemSchema],
directory: isarPath.path,
)
: null,
applicationDirectory,
))
], ],
child: AdaptiveLayoutBuilder( child: AdaptiveLayoutBuilder(
child: (context) => const Main(), child: (context) => const Main(),
@ -297,6 +282,7 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
}, },
builder: (context, child) => LocalizationContextWrapper( builder: (context, child) => LocalizationContextWrapper(
child: ScaffoldMessenger(child: child ?? Container()), child: ScaffoldMessenger(child: child ?? Container()),
currentLocale: language,
), ),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
darkTheme: darkTheme.copyWith( darkTheme: darkTheme.copyWith(

View file

@ -26,7 +26,6 @@ class CredentialsModel {
Map<String, String> header(Ref ref) { Map<String, String> header(Ref ref) {
final application = ref.read(applicationInfoProvider); final application = ref.read(applicationInfoProvider);
final headers = { final headers = {
'content-type': 'application/json',
'authorization': 'authorization':
'MediaBrowser Token="$token", Client="${application.name}", Device="${application.os}", DeviceId="$deviceId", Version="${application.version}"' 'MediaBrowser Token="$token", Client="${application.name}", Device="${application.os}", DeviceId="$deviceId", Version="${application.version}"'
}; };

View file

@ -45,6 +45,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
final String? seriesName; final String? seriesName;
final int season; final int season;
final int episode; final int episode;
final int? episodeEnd;
final List<Chapter> chapters; final List<Chapter> chapters;
final ItemLocation? location; final ItemLocation? location;
final DateTime? dateAired; final DateTime? dateAired;
@ -52,6 +53,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
required this.seriesName, required this.seriesName,
required this.season, required this.season,
required this.episode, required this.episode,
required this.episodeEnd,
this.chapters = const [], this.chapters = const [],
this.location, this.location,
this.dateAired, this.dateAired,
@ -134,12 +136,26 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
String seasonAnnotation(BuildContext context) => context.localized.season(1)[0]; String seasonAnnotation(BuildContext context) => context.localized.season(1)[0];
String episodeAnnotation(BuildContext context) => context.localized.episode(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) { String seasonEpisodeLabel(BuildContext context) {
return "${seasonAnnotation(context)}$season - ${episodeAnnotation(context)}$episode"; return "${seasonAnnotation(context)}$season - ${episodeAnnotation(context)}$episodeRange";
} }
String seasonEpisodeLabelFull(BuildContext context) { 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) { String episodeLabel(BuildContext context) {
@ -147,7 +163,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
} }
String get fullName { String get fullName {
return "$episode. $subText"; return "$episodeRange. $subText";
} }
@override @override
@ -169,6 +185,7 @@ class EpisodeModel extends ItemStreamModel with EpisodeModelMappable {
primaryRatio: item.primaryImageAspectRatio, primaryRatio: item.primaryImageAspectRatio,
season: item.parentIndexNumber ?? 0, season: item.parentIndexNumber ?? 0,
episode: item.indexNumber ?? 0, episode: item.indexNumber ?? 0,
episodeEnd: item.indexNumberEnd,
location: ItemLocation.fromDto(item.locationType), location: ItemLocation.fromDto(item.locationType),
parentImages: ImagesData.fromBaseItemParent(item, ref), parentImages: ImagesData.fromBaseItemParent(item, ref),
canDelete: item.canDelete, canDelete: item.canDelete,

View file

@ -31,6 +31,9 @@ class EpisodeModelMapper extends SubClassMapperBase<EpisodeModel> {
static int _$episode(EpisodeModel v) => v.episode; static int _$episode(EpisodeModel v) => v.episode;
static const Field<EpisodeModel, int> _f$episode = static const Field<EpisodeModel, int> _f$episode =
Field('episode', _$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 List<Chapter> _$chapters(EpisodeModel v) => v.chapters;
static const Field<EpisodeModel, List<Chapter>> _f$chapters = static const Field<EpisodeModel, List<Chapter>> _f$chapters =
Field('chapters', _$chapters, opt: true, def: const []); Field('chapters', _$chapters, opt: true, def: const []);
@ -86,6 +89,7 @@ class EpisodeModelMapper extends SubClassMapperBase<EpisodeModel> {
#seriesName: _f$seriesName, #seriesName: _f$seriesName,
#season: _f$season, #season: _f$season,
#episode: _f$episode, #episode: _f$episode,
#episodeEnd: _f$episodeEnd,
#chapters: _f$chapters, #chapters: _f$chapters,
#location: _f$location, #location: _f$location,
#dateAired: _f$dateAired, #dateAired: _f$dateAired,
@ -120,6 +124,7 @@ class EpisodeModelMapper extends SubClassMapperBase<EpisodeModel> {
seriesName: data.dec(_f$seriesName), seriesName: data.dec(_f$seriesName),
season: data.dec(_f$season), season: data.dec(_f$season),
episode: data.dec(_f$episode), episode: data.dec(_f$episode),
episodeEnd: data.dec(_f$episodeEnd),
chapters: data.dec(_f$chapters), chapters: data.dec(_f$chapters),
location: data.dec(_f$location), location: data.dec(_f$location),
dateAired: data.dec(_f$dateAired), dateAired: data.dec(_f$dateAired),
@ -166,6 +171,7 @@ abstract class EpisodeModelCopyWith<$R, $In extends EpisodeModel, $Out>
{String? seriesName, {String? seriesName,
int? season, int? season,
int? episode, int? episode,
int? episodeEnd,
List<Chapter>? chapters, List<Chapter>? chapters,
ItemLocation? location, ItemLocation? location,
DateTime? dateAired, DateTime? dateAired,
@ -209,6 +215,7 @@ class _EpisodeModelCopyWithImpl<$R, $Out>
{Object? seriesName = $none, {Object? seriesName = $none,
int? season, int? season,
int? episode, int? episode,
Object? episodeEnd = $none,
List<Chapter>? chapters, List<Chapter>? chapters,
Object? location = $none, Object? location = $none,
Object? dateAired = $none, Object? dateAired = $none,
@ -230,6 +237,7 @@ class _EpisodeModelCopyWithImpl<$R, $Out>
if (seriesName != $none) #seriesName: seriesName, if (seriesName != $none) #seriesName: seriesName,
if (season != null) #season: season, if (season != null) #season: season,
if (episode != null) #episode: episode, if (episode != null) #episode: episode,
if (episodeEnd != $none) #episodeEnd: episodeEnd,
if (chapters != null) #chapters: chapters, if (chapters != null) #chapters: chapters,
if (location != $none) #location: location, if (location != $none) #location: location,
if (dateAired != $none) #dateAired: dateAired, if (dateAired != $none) #dateAired: dateAired,
@ -253,6 +261,7 @@ class _EpisodeModelCopyWithImpl<$R, $Out>
seriesName: data.get(#seriesName, or: $value.seriesName), seriesName: data.get(#seriesName, or: $value.seriesName),
season: data.get(#season, or: $value.season), season: data.get(#season, or: $value.season),
episode: data.get(#episode, or: $value.episode), episode: data.get(#episode, or: $value.episode),
episodeEnd: data.get(#episodeEnd, or: $value.episodeEnd),
chapters: data.get(#chapters, or: $value.chapters), chapters: data.get(#chapters, or: $value.chapters),
location: data.get(#location, or: $value.location), location: data.get(#location, or: $value.location),
dateAired: data.get(#dateAired, or: $value.dateAired), dateAired: data.get(#dateAired, or: $value.dateAired),

View file

@ -50,6 +50,21 @@ class UserData with UserDataMappable {
Duration get playBackPosition => Duration(milliseconds: playbackPositionTicks ~/ 10000); 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.fromMap(Map<String, dynamic> map) => UserDataMapper.fromMap(map);
factory UserData.fromJson(String json) => UserDataMapper.fromJson(json); factory UserData.fromJson(String json) => UserDataMapper.fromJson(json);
} }

View file

@ -74,6 +74,9 @@ class SeasonModel extends ItemBaseModel with SeasonModelMappable {
episodes.firstWhereOrNull((element) => element.userData.played == false); episodes.firstWhereOrNull((element) => element.userData.played == false);
} }
@override
bool get syncAble => episodes.isNotEmpty && episodes.any((element) => element.syncAble);
@override @override
ImagesData? get getPosters => images ?? parentImages; ImagesData? get getPosters => images ?? parentImages;

View file

@ -2,6 +2,7 @@ import 'dart:developer';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:chopper/chopper.dart'; import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/api_provider.dart';
import 'package:fladder/providers/service_provider.dart'; import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_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/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart'; import 'package:fladder/providers/video_player_provider.dart';
@ -129,7 +129,7 @@ class PlaybackModelHelper {
await _createOfflinePlaybackModel( await _createOfflinePlaybackModel(
newItem, newItem,
null, null,
ref.read(syncProvider.notifier).getSyncedItem(newItem), await ref.read(syncProvider.notifier).getSyncedItem(newItem),
oldModel: currentModel, oldModel: currentModel,
); );
if (newModel == null) return null; if (newModel == null) return null;
@ -143,10 +143,10 @@ class PlaybackModelHelper {
SyncedItem? syncedItem, { SyncedItem? syncedItem, {
PlaybackModel? oldModel, PlaybackModel? oldModel,
}) async { }) 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; 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 syncedItems = children.where((element) => element.videoFile.existsSync()).toList();
final itemQueue = syncedItems.map((e) => e.createItemModel(ref)); final itemQueue = syncedItems.map((e) => e.createItemModel(ref));
@ -174,67 +174,81 @@ class PlaybackModelHelper {
final userId = ref.read(userProvider)?.id; final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return null; 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) { final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (queue.whereType<EpisodeModel>().toList().nextUp), SeriesModel _ || SeasonModel _ => (queue.whereType<EpisodeModel>().toList().nextUp),
_ => item, _ => 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
}; };
} else {
return (await _createServerPlaybackModel( if (firstItemToPlay == null) return null;
fullItem,
item.streamModel, final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body;
PlaybackType.directStream,
startPosition: startPosition, if (fullItem == null) return null;
oldModel: oldModel,
libraryQueue: queue, SyncedItem? syncedItem = await ref.read(syncProvider.notifier).getSyncedItem(fullItem);
)) ??
await _createOfflinePlaybackModel( final firstItemIsSynced = syncedItem != null && syncedItem.status == TaskStatus.complete;
fullItem,
item.streamModel, final options = {
syncedItem, 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;
} }
} }

View file

@ -0,0 +1,193 @@
import 'dart:convert';
import 'package:drift/drift.dart';
import 'package:drift_flutter/drift_flutter.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:path_provider/path_provider.dart';
import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/media_segments_model.dart';
import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/user_provider.dart';
part 'database_item.g.dart';
@TableIndex(name: 'database_id', columns: {#id})
class DatabaseItems extends Table {
TextColumn get userId => text()();
TextColumn get id => text().withLength(min: 1)();
BoolColumn get syncing => boolean()();
TextColumn get sortName => text().nullable()();
TextColumn get parentId => text().nullable()();
TextColumn get path => text().nullable()();
IntColumn get fileSize => integer().nullable()();
TextColumn get videoFileName => text().nullable()();
TextColumn get trickPlayModel => text().nullable()();
TextColumn get mediaSegments => text().nullable()();
TextColumn get images => text().nullable()();
TextColumn get chapters => text().nullable()();
TextColumn get subtitles => text().nullable()();
TextColumn get userData => text().nullable()();
@override
Set<Column<Object>> get primaryKey => {id, userId};
}
@DriftDatabase(tables: [DatabaseItems])
class AppDatabase extends _$AppDatabase {
AppDatabase(this.ref, [QueryExecutor? executor]) : super(executor ?? _openConnection());
final Ref ref;
String get userId => ref.read(userProvider.select((value) => value?.id ?? ""));
@override
int get schemaVersion => 1;
Future<void> clearDatabase() {
return transaction(() async {
for (final table in allTables) {
await delete(table).go();
}
});
}
Selectable<SyncedItem> getItem(String id) =>
(select(databaseItems)..where((tbl) => tbl.id.equals(id) & tbl.userId.equals(userId))).map(databaseConverter);
Selectable<SyncedItem> getParent(String id) =>
(select(databaseItems)..where((tbl) => tbl.parentId.equals(id) & tbl.userId.equals(userId)))
.map(databaseConverter);
Selectable<SyncedItem> get getParentItems =>
((select(databaseItems)..where((tbl) => (tbl.parentId.isNull() & tbl.userId.equals(userId))))
..orderBy([(t) => OrderingTerm(expression: t.sortName)]))
.map(databaseConverter);
Selectable<SyncedItem> getChildren(String parentId) =>
((select(databaseItems)..where((tbl) => (tbl.parentId.equals(parentId) & tbl.userId.equals(userId))))
..orderBy([(t) => OrderingTerm(expression: t.sortName)]))
.map(databaseConverter);
Future<int> insertItem(SyncedItem item) async {
final itemExists = await getItem(item.id).getSingleOrNull();
if (itemExists != null) {
return (update(databaseItems)..where((tbl) => tbl.id.equals(item.id) & tbl.userId.equals(userId)))
.write(toDataBaseItem(item));
} else {
return into(databaseItems).insert(toDataBaseItem(item));
}
}
Future<List<SyncedItem>> getNestedChildren(SyncedItem root) async {
final itemType = root.createItemModel(ref)?.type;
if (itemType == null) return [];
final int maxDepth = switch (itemType) {
FladderItemType.episode => 0,
FladderItemType.movie => 0,
FladderItemType.season => 1,
FladderItemType.series => 2,
_ => 1,
};
final all = <SyncedItem>[];
List<SyncedItem> toProcess = [root];
if (maxDepth == 0) {
return [];
}
for (var i = 0; i < maxDepth; i++) {
final futures = toProcess.map((item) => getChildren(item.id).get());
final resultsList = await Future.wait(futures);
final children = resultsList.expand((r) => r).toList();
if (children.isEmpty) break;
all.addAll(children);
toProcess = children;
}
return all;
}
Future<void> insertMultipleEntries(List<SyncedItem> items) async {
await batch((batch) {
batch.insertAll(
databaseItems,
items.map(toDataBaseItem),
mode: InsertMode.insertOrReplace,
);
});
}
Future<void> deleteAllItems(List<SyncedItem> items) async => await batch((batch) {
batch.deleteWhere(databaseItems, (tbl) => tbl.id.isIn(items.map((e) => e.id)));
});
DatabaseItemsCompanion toDataBaseItem(SyncedItem item) {
return DatabaseItemsCompanion(
id: Value(item.id),
parentId: Value(item.parentId),
syncing: Value(item.syncing),
userId: Value(userId),
path: Value(item.path),
fileSize: Value(item.fileSize),
sortName: Value(item.sortName),
videoFileName: Value(item.videoFileName),
trickPlayModel: Value(item.fTrickPlayModel != null ? jsonEncode(item.fTrickPlayModel?.toJson()) : null),
mediaSegments: Value(item.mediaSegments != null ? jsonEncode(item.mediaSegments?.toJson()) : null),
images: Value(item.fImages != null ? jsonEncode(item.fImages?.toJson()) : null),
chapters: Value(jsonEncode(item.fChapters.map((e) => e.toJson()).toList())),
subtitles: Value(jsonEncode(item.subtitles.map((e) => e.toJson()).toList())),
userData: Value(item.userData != null ? jsonEncode(item.userData?.toJson()) : null),
);
}
SyncedItem databaseConverter(DatabaseItem dataItem) {
final syncedItem = SyncedItem(
id: dataItem.id,
userId: dataItem.userId,
parentId: dataItem.parentId,
sortName: dataItem.sortName,
syncing: dataItem.syncing,
path: dataItem.path,
fileSize: dataItem.fileSize,
videoFileName: dataItem.videoFileName,
fTrickPlayModel:
dataItem.trickPlayModel != null ? TrickPlayModel.fromJson(jsonDecode(dataItem.trickPlayModel!)) : null,
mediaSegments:
dataItem.mediaSegments != null ? MediaSegmentsModel.fromJson(jsonDecode(dataItem.mediaSegments!)) : null,
fImages: dataItem.images != null ? ImagesData.fromJson(jsonDecode(dataItem.images!)) : null,
fChapters: (dataItem.chapters != null && dataItem.chapters!.isNotEmpty)
? (jsonDecode(dataItem.chapters!) as List).map((e) => Chapter.fromJson(e)).toList()
: [],
subtitles: (dataItem.subtitles != null && dataItem.subtitles!.isNotEmpty)
? (jsonDecode(dataItem.subtitles!) as List).map((e) => SubStreamModel.fromJson(e)).toList()
: [],
userData: dataItem.userData != null ? UserData.fromJson(jsonDecode(dataItem.userData!)) : null,
);
return syncedItem.copyWith(
itemModel: syncedItem.createItemModel(ref),
);
}
static QueryExecutor _openConnection() {
return driftDatabase(
name: 'syncedDatabase',
native: const DriftNativeOptions(
databaseDirectory: getApplicationSupportDirectory,
),
// If you need web support, see https://drift.simonbinder.eu/platforms/web/
);
}
}

File diff suppressed because it is too large Load diff

View file

@ -6,30 +6,13 @@ import 'package:fladder/models/syncing/sync_item.dart';
part 'i_synced_item.g.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 @collection
class ISyncedItem { class ISyncedItem {
String? userId; String? userId;
String id; String id;
bool syncing; bool syncing;
String? sortName; String? sortName;
@Index()
String? parentId; String? parentId;
String? path; String? path;
int? fileSize; int? fileSize;

View file

@ -77,7 +77,16 @@ const ISyncedItemSchema = IsarGeneratedSchema(
type: IsarType.string, type: IsarType.string,
), ),
], ],
indexes: [], indexes: [
IsarIndexSchema(
name: 'parentId',
properties: [
"parentId",
],
unique: false,
hash: false,
),
],
), ),
converter: IsarObjectConverter<String, ISyncedItem>( converter: IsarObjectConverter<String, ISyncedItem>(
serialize: serializeISyncedItem, serialize: serializeISyncedItem,

View file

@ -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/media_streams_model.dart';
import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/i_synced_item.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/providers/sync_provider.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
@ -44,9 +43,12 @@ class SyncedItem with _$SyncedItem {
@Default([]) List<Chapter> fChapters, @Default([]) List<Chapter> fChapters,
@Default([]) List<SubStreamModel> subtitles, @Default([]) List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData, @UserDataJsonSerializer() UserData? userData,
// ignore: invalid_annotation_target
@JsonKey(includeFromJson: false, includeToJson: false) ItemBaseModel? itemModel,
}) = _SyncItem; }) = _SyncItem;
static String trickPlayPath = "TrickPlay"; static String trickPlayPath = "TrickPlay";
static String chaptersPath = "Chapters";
List<Chapter> get chapters => fChapters.map((e) => e.copyWith(imageUrl: joinAll({"$path", e.imageUrl}))).toList(); List<Chapter> get chapters => fChapters.map((e) => e.copyWith(imageUrl: joinAll({"$path", e.imageUrl}))).toList();
@ -69,9 +71,9 @@ class SyncedItem with _$SyncedItem {
File get videoFile => File(joinAll(["$path", "$videoFileName"])); File get videoFile => File(joinAll(["$path", "$videoFileName"]));
Directory get directory => Directory(path ?? ""); Directory get directory => Directory(path ?? "");
SyncStatus get status => switch (videoFile.existsSync()) { TaskStatus get status => switch (videoFile.existsSync()) {
true => SyncStatus.complete, true => TaskStatus.complete,
_ => SyncStatus.partially, _ => TaskStatus.notFound,
}; };
String? get taskId => task?.taskId; String? get taskId => task?.taskId;
@ -94,6 +96,7 @@ class SyncedItem with _$SyncedItem {
try { try {
await videoFile.delete(); await videoFile.delete();
await Directory(joinAll([directory.path, trickPlayPath])).delete(recursive: true); await Directory(joinAll([directory.path, trickPlayPath])).delete(recursive: true);
await Directory(joinAll([directory.path, chaptersPath])).delete(recursive: true);
} catch (e) { } catch (e) {
return false; return false;
} }
@ -101,10 +104,9 @@ class SyncedItem with _$SyncedItem {
return true; return true;
} }
List<SyncedItem> nestedChildren(WidgetRef ref) => ref.watch(syncChildrenProvider(this)); Future<List<SyncedItem>> getChildren(Ref ref) async => await ref.read(syncProvider.notifier).getChildren(this);
Future<List<SyncedItem>> getNestedChildren(Ref ref) async =>
List<SyncedItem> getChildren(Ref ref) => ref.read(syncProvider.notifier).getChildren(this); await ref.read(syncProvider.notifier).getNestedChildren(this);
List<SyncedItem> getNestedChildren(Ref ref) => ref.read(syncProvider.notifier).getNestedChildren(this);
Future<int> get getDirSize async { Future<int> get getDirSize async {
var files = await directory.list(recursive: true).toList(); var files = await directory.list(recursive: true).toList();
@ -156,44 +158,45 @@ class SyncedItem with _$SyncedItem {
} }
} }
enum SyncStatus {
complete(
Color.fromARGB(255, 141, 214, 58),
IconsaxPlusLinear.tick_circle,
),
partially(
Color.fromARGB(255, 221, 135, 23),
IconsaxPlusLinear.more_circle,
),
;
const SyncStatus(this.color, this.icon);
final Color color;
String label(BuildContext context) {
return switch (this) {
SyncStatus.partially => context.localized.syncStatusPartially,
SyncStatus.complete => context.localized.syncStatusSynced,
};
}
final IconData icon;
}
extension StatusExtension on TaskStatus { extension StatusExtension on TaskStatus {
Color color(BuildContext context) => switch (this) { IconData get icon => switch (this) {
TaskStatus.enqueued => Colors.blueAccent, TaskStatus.enqueued => IconsaxPlusLinear.calendar_circle,
TaskStatus.running => Colors.limeAccent, TaskStatus.running => IconsaxPlusLinear.arrow_down_1,
TaskStatus.complete => Colors.limeAccent, TaskStatus.complete => IconsaxPlusLinear.tick_circle,
TaskStatus.canceled || TaskStatus.notFound || TaskStatus.failed => Theme.of(context).colorScheme.error, TaskStatus.notFound => IconsaxPlusLinear.warning_2,
TaskStatus.waitingToRetry => Colors.yellowAccent, TaskStatus.failed => IconsaxPlusLinear.tag_cross,
TaskStatus.paused => Colors.orangeAccent, 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) { String name(BuildContext context) => switch (this) {
TaskStatus.enqueued => context.localized.syncStatusEnqueued, TaskStatus.enqueued => context.localized.syncStatusEnqueued,
TaskStatus.running => context.localized.syncStatusRunning, TaskStatus.running => context.localized.syncStatusRunning,
TaskStatus.complete => context.localized.syncStatusComplete, TaskStatus.complete => context.localized.syncStatusSynced,
TaskStatus.notFound => context.localized.syncStatusNotFound, TaskStatus.notFound => context.localized.syncStatusNotFound,
TaskStatus.failed => context.localized.syncStatusFailed, TaskStatus.failed => context.localized.syncStatusFailed,
TaskStatus.canceled => context.localized.syncStatusCanceled, TaskStatus.canceled => context.localized.syncStatusCanceled,

View file

@ -31,7 +31,10 @@ mixin _$SyncedItem {
List<Chapter> get fChapters => throw _privateConstructorUsedError; List<Chapter> get fChapters => throw _privateConstructorUsedError;
List<SubStreamModel> get subtitles => throw _privateConstructorUsedError; List<SubStreamModel> get subtitles => throw _privateConstructorUsedError;
@UserDataJsonSerializer() @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 /// Create a copy of SyncedItem
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.
@ -61,7 +64,9 @@ abstract class $SyncedItemCopyWith<$Res> {
ImagesData? fImages, ImagesData? fImages,
List<Chapter> fChapters, List<Chapter> fChapters,
List<SubStreamModel> subtitles, List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData}); @UserDataJsonSerializer() UserData? userData,
@JsonKey(includeFromJson: false, includeToJson: false)
ItemBaseModel? itemModel});
$TrickPlayModelCopyWith<$Res>? get fTrickPlayModel; $TrickPlayModelCopyWith<$Res>? get fTrickPlayModel;
} }
@ -96,6 +101,7 @@ class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem>
Object? fChapters = null, Object? fChapters = null,
Object? subtitles = null, Object? subtitles = null,
Object? userData = freezed, Object? userData = freezed,
Object? itemModel = freezed,
}) { }) {
return _then(_value.copyWith( return _then(_value.copyWith(
id: null == id id: null == id
@ -158,6 +164,10 @@ class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem>
? _value.userData ? _value.userData
: userData // ignore: cast_nullable_to_non_nullable : userData // ignore: cast_nullable_to_non_nullable
as UserData?, as UserData?,
itemModel: freezed == itemModel
? _value.itemModel
: itemModel // ignore: cast_nullable_to_non_nullable
as ItemBaseModel?,
) as $Val); ) as $Val);
} }
@ -199,7 +209,9 @@ abstract class _$$SyncItemImplCopyWith<$Res>
ImagesData? fImages, ImagesData? fImages,
List<Chapter> fChapters, List<Chapter> fChapters,
List<SubStreamModel> subtitles, List<SubStreamModel> subtitles,
@UserDataJsonSerializer() UserData? userData}); @UserDataJsonSerializer() UserData? userData,
@JsonKey(includeFromJson: false, includeToJson: false)
ItemBaseModel? itemModel});
@override @override
$TrickPlayModelCopyWith<$Res>? get fTrickPlayModel; $TrickPlayModelCopyWith<$Res>? get fTrickPlayModel;
@ -233,6 +245,7 @@ class __$$SyncItemImplCopyWithImpl<$Res>
Object? fChapters = null, Object? fChapters = null,
Object? subtitles = null, Object? subtitles = null,
Object? userData = freezed, Object? userData = freezed,
Object? itemModel = freezed,
}) { }) {
return _then(_$SyncItemImpl( return _then(_$SyncItemImpl(
id: null == id id: null == id
@ -295,6 +308,10 @@ class __$$SyncItemImplCopyWithImpl<$Res>
? _value.userData ? _value.userData
: userData // ignore: cast_nullable_to_non_nullable : userData // ignore: cast_nullable_to_non_nullable
as UserData?, 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, this.fImages,
final List<Chapter> fChapters = const [], final List<Chapter> fChapters = const [],
final List<SubStreamModel> subtitles = const [], final List<SubStreamModel> subtitles = const [],
@UserDataJsonSerializer() this.userData}) @UserDataJsonSerializer() this.userData,
@JsonKey(includeFromJson: false, includeToJson: false) this.itemModel})
: _fChapters = fChapters, : _fChapters = fChapters,
_subtitles = subtitles, _subtitles = subtitles,
super._(); super._();
@ -369,10 +387,14 @@ class _$SyncItemImpl extends _SyncItem {
@override @override
@UserDataJsonSerializer() @UserDataJsonSerializer()
final UserData? userData; final UserData? userData;
// ignore: invalid_annotation_target
@override
@JsonKey(includeFromJson: false, includeToJson: false)
final ItemBaseModel? itemModel;
@override @override
String toString() { 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 /// Create a copy of SyncedItem
@ -400,7 +422,9 @@ abstract class _SyncItem extends SyncedItem {
final ImagesData? fImages, final ImagesData? fImages,
final List<Chapter> fChapters, final List<Chapter> fChapters,
final List<SubStreamModel> subtitles, final List<SubStreamModel> subtitles,
@UserDataJsonSerializer() final UserData? userData}) = _$SyncItemImpl; @UserDataJsonSerializer() final UserData? userData,
@JsonKey(includeFromJson: false, includeToJson: false)
final ItemBaseModel? itemModel}) = _$SyncItemImpl;
_SyncItem._() : super._(); _SyncItem._() : super._();
@override @override
@ -433,7 +457,10 @@ abstract class _SyncItem extends SyncedItem {
List<SubStreamModel> get subtitles; List<SubStreamModel> get subtitles;
@override @override
@UserDataJsonSerializer() @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 /// Create a copy of SyncedItem
/// with the given fields replaced by the non-null parameter values. /// with the given fields replaced by the non-null parameter values.

View file

@ -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 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; if (episodeModel == null) return;
final seriesSyncedItem = syncNotifier.getSyncedItem(episodeModel.parentBaseModel); final seriesSyncedItem = await syncNotifier.getSyncedItem(episodeModel.parentBaseModel);
if (seriesSyncedItem == null) return; if (seriesSyncedItem == null) return;
final seriesModel = seriesSyncedItem.createItemModel(ref) as SeriesModel?; final seriesModel = seriesSyncedItem.itemModel as SeriesModel?;
if (seriesModel == null) return; if (seriesModel == null) return;
final episodes = syncNotifier final episodes = (await syncNotifier.getNestedChildren(seriesSyncedItem))
.getNestedChildren(seriesSyncedItem) .map((e) => e.itemModel)
.map(
(e) => e.createItemModel(ref),
)
.nonNulls
.whereType<EpisodeModel>() .whereType<EpisodeModel>()
.toList(); .toList();
state = state.copyWith( state = state.copyWith(

View file

@ -35,11 +35,11 @@ class MovieDetails extends _$MovieDetails {
} }
} }
void _tryToCreateOfflineState(ItemBaseModel item) { void _tryToCreateOfflineState(ItemBaseModel item) async {
final syncNotifier = ref.read(syncProvider.notifier); final syncNotifier = ref.read(syncProvider.notifier);
final syncedItem = syncNotifier.getParentItem(item.id); final syncedItem = await syncNotifier.getParentItem(item.id);
if (syncedItem == null) return; if (syncedItem == null) return;
final movieModel = syncedItem.createItemModel(ref) as MovieModel; final movieModel = syncedItem.itemModel as MovieModel?;
state = movieModel; state = movieModel;
} }

View file

@ -6,7 +6,7 @@ part of 'movies_details_provider.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$movieDetailsHash() => r'872ea61464ef8493c7e6c559c526377f1c8f6a6d'; String _$movieDetailsHash() => r'a9d8d2eeb7fa37652f25c1820b5e346efeeb59fc';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {

View file

@ -29,7 +29,12 @@ class SeasonDetailsNotifier extends StateNotifier<SeasonModel?> {
seriesId: newState?.seriesId ?? "", seriesId: newState?.seriesId ?? "",
seasonId: newState?.id, seasonId: newState?.id,
season: newState?.season, 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()); newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref).toList());
state = newState; state = newState;

View file

@ -32,20 +32,39 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
final response = await api.usersUserIdItemsItemIdGet(itemId: seriesModel.id); final response = await api.usersUserIdItemsItemIdGet(itemId: seriesModel.id);
if (response.body == null) return null; if (response.body == null) return null;
newState = response.bodyOrThrow as SeriesModel; newState = response.bodyOrThrow as SeriesModel;
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id);
newState = newState.copyWith(seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref));
final episodes = await api.showsSeriesIdEpisodesGet(seriesId: seriesModel.id, fields: [ final episodes = await api.showsSeriesIdEpisodesGet(seriesId: seriesModel.id, fields: [
ItemFields.mediastreams, ItemFields.mediastreams,
ItemFields.mediasources, ItemFields.mediasources,
ItemFields.overview, ItemFields.overview,
ItemFields.candownload,
ItemFields.childcount,
]); ]);
final newEpisodes = EpisodeModel.episodesFromDto(
episodes.body?.items,
ref,
);
final episodesCanDownload = newEpisodes.any((episode) => episode.canDownload == true);
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id, fields: [
ItemFields.mediastreams,
ItemFields.mediasources,
ItemFields.overview,
ItemFields.candownload,
ItemFields.childcount,
]);
newState = newState.copyWith( newState = newState.copyWith(
availableEpisodes: EpisodeModel.episodesFromDto( seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref)
episodes.body?.items, .map((element) => element.copyWith(
ref, canDownload: true,
), episodes: newEpisodes.where((episode) => episode.season == element.season).toList(),
))
.toList(),
);
newState = newState.copyWith(
canDownload: episodesCanDownload,
availableEpisodes: newEpisodes,
); );
final related = await ref.read(relatedUtilityProvider).relatedContent(seriesModel.id); final related = await ref.read(relatedUtilityProvider).relatedContent(seriesModel.id);
@ -59,20 +78,16 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
Future<void> _tryToCreateOfflineState(ItemBaseModel series) async { Future<void> _tryToCreateOfflineState(ItemBaseModel series) async {
final syncNotifier = ref.read(syncProvider.notifier); final syncNotifier = ref.read(syncProvider.notifier);
final syncedItem = syncNotifier.getSyncedItem(series); final syncedItem = await syncNotifier.getSyncedItem(series);
if (syncedItem == null) return; if (syncedItem == null) return;
final seriesModel = syncedItem.createItemModel(ref) as SeriesModel; final seriesModel = syncedItem.itemModel as SeriesModel;
final allChildren = syncedItem final allChildren = (await syncedItem.getNestedChildren(ref)).map((e) => e.itemModel).toList();
.getNestedChildren(ref) if (mounted) {
.map( state = seriesModel.copyWith(
(e) => e.createItemModel(ref), availableEpisodes: allChildren.whereType<EpisodeModel>().toList(),
) seasons: allChildren.whereType<SeasonModel>().toList(),
.nonNulls );
.toList(); }
state = seriesModel.copyWith(
availableEpisodes: allChildren.whereType<EpisodeModel>().toList(),
seasons: allChildren.whereType<SeasonModel>().toList(),
);
return; return;
} }

View file

@ -536,7 +536,10 @@ class JellyService {
return api.showsSeriesIdEpisodesGet( return api.showsSeriesIdEpisodesGet(
seriesId: seriesId, seriesId: seriesId,
userId: account?.id, userId: account?.id,
fields: fields, fields: [
...?fields,
ItemFields.parentid,
],
isMissing: isMissing, isMissing: isMissing,
limit: limit, limit: limit,
sortBy: sortBy, sortBy: sortBy,
@ -694,7 +697,10 @@ class JellyService {
seriesId: seriesId, seriesId: seriesId,
isMissing: isMissing, isMissing: isMissing,
enableUserData: enableUserData, enableUserData: enableUserData,
fields: fields, fields: [
...?fields,
ItemFields.parentid,
],
); );
Future<Response<QueryFilters>> itemsFilters2Get({ Future<Response<QueryFilters>> itemsFilters2Get({
@ -903,37 +909,37 @@ class JellyService {
Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId); Future<Response<dynamic>> deleteItem(String itemId) => api.itemsItemIdDelete(itemId: itemId);
Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async { Future<UserConfiguration?> _updateUserConfiguration(UserConfiguration newUserConfiguration) async {
if (account?.id == null) return null; if (account?.id == null) return null;
final response = await api.usersConfigurationPost( final response = await api.usersConfigurationPost(
userId: account!.id, userId: account!.id,
body: newUserConfiguration, body: newUserConfiguration,
); );
if (response.isSuccessful) { if (response.isSuccessful) {
return newUserConfiguration; return newUserConfiguration;
}
return null;
} }
return null;
}
Future<UserConfiguration?> updateRememberAudioSelections() { Future<UserConfiguration?> updateRememberAudioSelections() {
final currentUserConfiguration = account?.userConfiguration; final currentUserConfiguration = account?.userConfiguration;
if (currentUserConfiguration == null) return Future.value(null); if (currentUserConfiguration == null) return Future.value(null);
final updated = currentUserConfiguration.copyWith( final updated = currentUserConfiguration.copyWith(
rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false), rememberAudioSelections: !(currentUserConfiguration.rememberAudioSelections ?? false),
); );
return _updateUserConfiguration(updated); return _updateUserConfiguration(updated);
} }
Future<UserConfiguration?> updateRememberSubtitleSelections() { Future<UserConfiguration?> updateRememberSubtitleSelections() {
final current = account?.userConfiguration; final current = account?.userConfiguration;
if (current == null) return Future.value(null); if (current == null) return Future.value(null);
final updated = current.copyWith( final updated = current.copyWith(
rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false), rememberSubtitleSelections: !(current.rememberSubtitleSelections ?? false),
); );
return _updateUserConfiguration(updated); return _updateUserConfiguration(updated);
} }
} }

View file

@ -1,7 +1,10 @@
import 'package:flutter/widgets.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/util/localization_helper.dart';
part 'background_download_provider.g.dart'; part 'background_download_provider.g.dart';
@ -14,13 +17,7 @@ class BackgroundDownloader extends _$BackgroundDownloader {
..configure( ..configure(
globalConfig: globalConfig(maxDownloads), globalConfig: globalConfig(maxDownloads),
) )
..trackTasks() ..trackTasks();
..configureNotification(
running: const TaskNotification('Downloading', 'file: {filename}'),
complete: const TaskNotification('Download finished', 'file: {filename}'),
paused: const TaskNotification('Download paused', 'file: {filename}'),
progressBar: true,
);
} }
void setMaxConcurrent(int value) { void setMaxConcurrent(int value) {
@ -29,6 +26,16 @@ class BackgroundDownloader extends _$BackgroundDownloader {
); );
} }
void updateTranslations(BuildContext context) async {
state.configureNotification(
running: TaskNotification(context.localized.notificationDownloadingDownloading, '{filename}\n{networkSpeed}'),
complete: TaskNotification(context.localized.notificationDownloadingFinished, '{filename}'),
paused: TaskNotification(context.localized.notificationDownloadingPaused, '{filename}'),
error: TaskNotification(context.localized.notificationDownloadingError, '{filename}'),
progressBar: true,
);
}
(String, dynamic) globalConfig(int value) => value == 0 (String, dynamic) globalConfig(int value) => value == 0
? ("", "") ? ("", "")
: ( : (

View file

@ -7,7 +7,7 @@ part of 'background_download_provider.dart';
// ************************************************************************** // **************************************************************************
String _$backgroundDownloaderHash() => String _$backgroundDownloaderHash() =>
r'dc27f708fc2f1695d37afcb99f8814bc024037af'; r'9d866549ed7632e855ba30de2765368960889cff';
/// See also [BackgroundDownloader]. /// See also [BackgroundDownloader].
@ProviderFor(BackgroundDownloader) @ProviderFor(BackgroundDownloader)

View file

@ -1,40 +1,41 @@
import 'package:isar/isar.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart'; import 'package: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/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_item.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
part 'sync_provider_helpers.g.dart'; part 'sync_provider_helpers.g.dart';
@riverpod @riverpod
class SyncChildren extends _$SyncChildren { Stream<SyncedItem?> syncedItem(Ref ref, ItemBaseModel? item) {
@override final id = item?.id;
List<SyncedItem> build(SyncedItem arg) { if (id == null || id.isEmpty) {
final syncedItemIsar = ref.watch(syncProvider.notifier).isar; return Stream.value(null);
final allChildren = <SyncedItem>[];
List<SyncedItem> toProcess = [arg];
while (toProcess.isNotEmpty) {
final currentLevel = toProcess.map(
(parent) {
final children = syncedItemIsar?.iSyncedItems.where().parentIdEqualTo(parent.id).sortBySortName().findAll();
return children?.map((e) => SyncedItem.fromIsar(e, ref.read(syncProvider.notifier).syncPath ?? "")) ??
<SyncedItem>[];
},
);
allChildren.addAll(currentLevel.expand((list) => list));
toProcess = currentLevel.expand((list) => list).toList();
}
return allChildren;
} }
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 @riverpod
class SyncDownloadStatus extends _$SyncDownloadStatus { class SyncDownloadStatus extends _$SyncDownloadStatus {
@override @override
DownloadStream? build(SyncedItem arg) { DownloadStream? build(SyncedItem arg, List<SyncedItem> children) {
final nestedChildren = ref.watch(syncChildrenProvider(arg)); final nestedChildren = children;
ref.watch(downloadTasksProvider(arg.id)); ref.watch(downloadTasksProvider(arg.id));
for (var element in nestedChildren) { for (var element in nestedChildren) {
@ -45,60 +46,53 @@ class SyncDownloadStatus extends _$SyncDownloadStatus {
int downloadCount = 0; int downloadCount = 0;
double fullProgress = mainStream.hasDownload ? mainStream.progress : 0.0; double fullProgress = mainStream.hasDownload ? mainStream.progress : 0.0;
int fullySyncedChildren = 0;
for (var i = 0; i < nestedChildren.length; i++) { for (var i = 0; i < nestedChildren.length; i++) {
final childItem = nestedChildren[i]; final childItem = nestedChildren[i];
final downloadStream = ref.read(downloadTasksProvider(childItem.id)); final downloadStream = ref.read(downloadTasksProvider(childItem.id));
if (childItem.videoFile.existsSync()) {
fullySyncedChildren++;
}
if (downloadStream.hasDownload) { if (downloadStream.hasDownload) {
downloadCount++; downloadCount++;
fullProgress += downloadStream.progress; 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( return mainStream.copyWith(
status: fullySynced ? TaskStatus.complete : mainStream.status,
progress: fullProgress / downloadCount.clamp(1, double.infinity).toInt(), progress: fullProgress / downloadCount.clamp(1, double.infinity).toInt(),
); );
} }
} }
@riverpod
class SyncStatuses extends _$SyncStatuses {
@override
FutureOr<SyncStatus> build(SyncedItem arg) async {
final nestedChildren = ref.watch(syncChildrenProvider(arg));
ref.watch(downloadTasksProvider(arg.id));
for (var element in nestedChildren) {
ref.watch(downloadTasksProvider(element.id));
}
for (var i = 0; i < nestedChildren.length; i++) {
final item = nestedChildren[i];
if (item.hasVideoFile && !await item.videoFile.exists()) {
return SyncStatus.partially;
}
}
if (arg.hasVideoFile && !await arg.videoFile.exists()) {
return SyncStatus.partially;
}
return SyncStatus.complete;
}
}
@riverpod @riverpod
class SyncSize extends _$SyncSize { class SyncSize extends _$SyncSize {
@override @override
int? build(SyncedItem arg) { int? build(SyncedItem arg, List<SyncedItem>? children) {
final nestedChildren = ref.watch(syncChildrenProvider(arg)); final nestedChildren = children;
ref.watch(downloadTasksProvider(arg.id)); ref.watch(downloadTasksProvider(arg.id));
for (var element in nestedChildren) {
ref.watch(downloadTasksProvider(element.id));
}
int size = arg.fileSize ?? 0; int size = arg.fileSize ?? 0;
for (var element in nestedChildren) {
size += element.fileSize ?? 0; if (nestedChildren != null) {
for (var element in nestedChildren) {
ref.watch(downloadTasksProvider(element.id));
}
for (var element in nestedChildren) {
if (element.videoFile.existsSync()) {
size += element.fileSize ?? 0;
}
}
} }
return size; return size;
} }
} }

View file

@ -6,7 +6,7 @@ part of 'sync_provider_helpers.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$syncChildrenHash() => r'f6fdb1aa36d6655976baa5fbe0d8a6b812d7e95b'; String _$syncedItemHash() => r'7b1178ba78529ebf65425aa4cb8b239a28c7914b';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@ -29,39 +29,30 @@ class _SystemHash {
} }
} }
abstract class _$SyncChildren /// See also [syncedItem].
extends BuildlessAutoDisposeNotifier<List<SyncedItem>> { @ProviderFor(syncedItem)
late final SyncedItem arg; const syncedItemProvider = SyncedItemFamily();
List<SyncedItem> build( /// See also [syncedItem].
SyncedItem arg, class SyncedItemFamily extends Family<AsyncValue<SyncedItem?>> {
); /// See also [syncedItem].
} const SyncedItemFamily();
/// See also [SyncChildren]. /// See also [syncedItem].
@ProviderFor(SyncChildren) SyncedItemProvider call(
const syncChildrenProvider = SyncChildrenFamily(); ItemBaseModel? item,
/// See also [SyncChildren].
class SyncChildrenFamily extends Family<List<SyncedItem>> {
/// See also [SyncChildren].
const SyncChildrenFamily();
/// See also [SyncChildren].
SyncChildrenProvider call(
SyncedItem arg,
) { ) {
return SyncChildrenProvider( return SyncedItemProvider(
arg, item,
); );
} }
@override @override
SyncChildrenProvider getProviderOverride( SyncedItemProvider getProviderOverride(
covariant SyncChildrenProvider provider, covariant SyncedItemProvider provider,
) { ) {
return call( return call(
provider.arg, provider.item,
); );
} }
@ -77,81 +68,75 @@ class SyncChildrenFamily extends Family<List<SyncedItem>> {
_allTransitiveDependencies; _allTransitiveDependencies;
@override @override
String? get name => r'syncChildrenProvider'; String? get name => r'syncedItemProvider';
} }
/// See also [SyncChildren]. /// See also [syncedItem].
class SyncChildrenProvider class SyncedItemProvider extends AutoDisposeStreamProvider<SyncedItem?> {
extends AutoDisposeNotifierProviderImpl<SyncChildren, List<SyncedItem>> { /// See also [syncedItem].
/// See also [SyncChildren]. SyncedItemProvider(
SyncChildrenProvider( ItemBaseModel? item,
SyncedItem arg,
) : this._internal( ) : this._internal(
() => SyncChildren()..arg = arg, (ref) => syncedItem(
from: syncChildrenProvider, ref as SyncedItemRef,
name: r'syncChildrenProvider', item,
),
from: syncedItemProvider,
name: r'syncedItemProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product') const bool.fromEnvironment('dart.vm.product')
? null ? null
: _$syncChildrenHash, : _$syncedItemHash,
dependencies: SyncChildrenFamily._dependencies, dependencies: SyncedItemFamily._dependencies,
allTransitiveDependencies: allTransitiveDependencies:
SyncChildrenFamily._allTransitiveDependencies, SyncedItemFamily._allTransitiveDependencies,
arg: arg, item: item,
); );
SyncChildrenProvider._internal( SyncedItemProvider._internal(
super._createNotifier, { super._createNotifier, {
required super.name, required super.name,
required super.dependencies, required super.dependencies,
required super.allTransitiveDependencies, required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.arg, required this.item,
}) : super.internal(); }) : super.internal();
final SyncedItem arg; final ItemBaseModel? item;
@override @override
List<SyncedItem> runNotifierBuild( Override overrideWith(
covariant SyncChildren notifier, Stream<SyncedItem?> Function(SyncedItemRef provider) create,
) { ) {
return notifier.build(
arg,
);
}
@override
Override overrideWith(SyncChildren Function() create) {
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SyncChildrenProvider._internal( override: SyncedItemProvider._internal(
() => create()..arg = arg, (ref) => create(ref as SyncedItemRef),
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
arg: arg, item: item,
), ),
); );
} }
@override @override
AutoDisposeNotifierProviderElement<SyncChildren, List<SyncedItem>> AutoDisposeStreamProviderElement<SyncedItem?> createElement() {
createElement() { return _SyncedItemProviderElement(this);
return _SyncChildrenProviderElement(this);
} }
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SyncChildrenProvider && other.arg == arg; return other is SyncedItemProvider && other.item == item;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode); hash = _SystemHash.combine(hash, item.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@ -159,29 +144,325 @@ class SyncChildrenProvider
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
mixin SyncChildrenRef on AutoDisposeNotifierProviderRef<List<SyncedItem>> { mixin SyncedItemRef on AutoDisposeStreamProviderRef<SyncedItem?> {
/// The parameter `arg` of this provider. /// The parameter `item` of this provider.
SyncedItem get arg; ItemBaseModel? get item;
} }
class _SyncChildrenProviderElement class _SyncedItemProviderElement
extends AutoDisposeNotifierProviderElement<SyncChildren, List<SyncedItem>> extends AutoDisposeStreamProviderElement<SyncedItem?> with SyncedItemRef {
with SyncChildrenRef { _SyncedItemProviderElement(super.provider);
_SyncChildrenProviderElement(super.provider);
@override @override
SyncedItem get arg => (origin as SyncChildrenProvider).arg; ItemBaseModel? get item => (origin as SyncedItemProvider).item;
}
String _$syncedChildrenHash() => r'2b6ce1611750785060df6317ce0ea25e2dc0aeb4';
abstract class _$SyncedChildren
extends BuildlessAutoDisposeAsyncNotifier<List<SyncedItem>> {
late final SyncedItem item;
FutureOr<List<SyncedItem>> build(
SyncedItem item,
);
}
/// See also [SyncedChildren].
@ProviderFor(SyncedChildren)
const syncedChildrenProvider = SyncedChildrenFamily();
/// See also [SyncedChildren].
class SyncedChildrenFamily extends Family<AsyncValue<List<SyncedItem>>> {
/// See also [SyncedChildren].
const SyncedChildrenFamily();
/// See also [SyncedChildren].
SyncedChildrenProvider call(
SyncedItem item,
) {
return SyncedChildrenProvider(
item,
);
}
@override
SyncedChildrenProvider getProviderOverride(
covariant SyncedChildrenProvider provider,
) {
return call(
provider.item,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'syncedChildrenProvider';
}
/// See also [SyncedChildren].
class SyncedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl<
SyncedChildren, List<SyncedItem>> {
/// See also [SyncedChildren].
SyncedChildrenProvider(
SyncedItem item,
) : this._internal(
() => SyncedChildren()..item = item,
from: syncedChildrenProvider,
name: r'syncedChildrenProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncedChildrenHash,
dependencies: SyncedChildrenFamily._dependencies,
allTransitiveDependencies:
SyncedChildrenFamily._allTransitiveDependencies,
item: item,
);
SyncedChildrenProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.item,
}) : super.internal();
final SyncedItem item;
@override
FutureOr<List<SyncedItem>> runNotifierBuild(
covariant SyncedChildren notifier,
) {
return notifier.build(
item,
);
}
@override
Override overrideWith(SyncedChildren Function() create) {
return ProviderOverride(
origin: this,
override: SyncedChildrenProvider._internal(
() => create()..item = item,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
item: item,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<SyncedChildren, List<SyncedItem>>
createElement() {
return _SyncedChildrenProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncedChildrenProvider && other.item == item;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, item.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SyncedChildrenRef
on AutoDisposeAsyncNotifierProviderRef<List<SyncedItem>> {
/// The parameter `item` of this provider.
SyncedItem get item;
}
class _SyncedChildrenProviderElement
extends AutoDisposeAsyncNotifierProviderElement<SyncedChildren,
List<SyncedItem>> with SyncedChildrenRef {
_SyncedChildrenProviderElement(super.provider);
@override
SyncedItem get item => (origin as SyncedChildrenProvider).item;
}
String _$syncedNestedChildrenHash() =>
r'ea8dd0e694efa6d6ec0c73d699b5fb3e933f9322';
abstract class _$SyncedNestedChildren
extends BuildlessAutoDisposeAsyncNotifier<List<SyncedItem>> {
late final SyncedItem item;
FutureOr<List<SyncedItem>> build(
SyncedItem item,
);
}
/// See also [SyncedNestedChildren].
@ProviderFor(SyncedNestedChildren)
const syncedNestedChildrenProvider = SyncedNestedChildrenFamily();
/// See also [SyncedNestedChildren].
class SyncedNestedChildrenFamily extends Family<AsyncValue<List<SyncedItem>>> {
/// See also [SyncedNestedChildren].
const SyncedNestedChildrenFamily();
/// See also [SyncedNestedChildren].
SyncedNestedChildrenProvider call(
SyncedItem item,
) {
return SyncedNestedChildrenProvider(
item,
);
}
@override
SyncedNestedChildrenProvider getProviderOverride(
covariant SyncedNestedChildrenProvider provider,
) {
return call(
provider.item,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'syncedNestedChildrenProvider';
}
/// See also [SyncedNestedChildren].
class SyncedNestedChildrenProvider extends AutoDisposeAsyncNotifierProviderImpl<
SyncedNestedChildren, List<SyncedItem>> {
/// See also [SyncedNestedChildren].
SyncedNestedChildrenProvider(
SyncedItem item,
) : this._internal(
() => SyncedNestedChildren()..item = item,
from: syncedNestedChildrenProvider,
name: r'syncedNestedChildrenProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncedNestedChildrenHash,
dependencies: SyncedNestedChildrenFamily._dependencies,
allTransitiveDependencies:
SyncedNestedChildrenFamily._allTransitiveDependencies,
item: item,
);
SyncedNestedChildrenProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.item,
}) : super.internal();
final SyncedItem item;
@override
FutureOr<List<SyncedItem>> runNotifierBuild(
covariant SyncedNestedChildren notifier,
) {
return notifier.build(
item,
);
}
@override
Override overrideWith(SyncedNestedChildren Function() create) {
return ProviderOverride(
origin: this,
override: SyncedNestedChildrenProvider._internal(
() => create()..item = item,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
item: item,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<SyncedNestedChildren,
List<SyncedItem>> createElement() {
return _SyncedNestedChildrenProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncedNestedChildrenProvider && other.item == item;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, item.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SyncedNestedChildrenRef
on AutoDisposeAsyncNotifierProviderRef<List<SyncedItem>> {
/// The parameter `item` of this provider.
SyncedItem get item;
}
class _SyncedNestedChildrenProviderElement
extends AutoDisposeAsyncNotifierProviderElement<SyncedNestedChildren,
List<SyncedItem>> with SyncedNestedChildrenRef {
_SyncedNestedChildrenProviderElement(super.provider);
@override
SyncedItem get item => (origin as SyncedNestedChildrenProvider).item;
} }
String _$syncDownloadStatusHash() => String _$syncDownloadStatusHash() =>
r'5a0f8537a977c52e6083bd84265631ea5d160637'; r'6ee039e094f1e007ebaeb20ae63430be829cdeb7';
abstract class _$SyncDownloadStatus abstract class _$SyncDownloadStatus
extends BuildlessAutoDisposeNotifier<DownloadStream?> { extends BuildlessAutoDisposeNotifier<DownloadStream?> {
late final SyncedItem arg; late final SyncedItem arg;
late final List<SyncedItem> children;
DownloadStream? build( DownloadStream? build(
SyncedItem arg, SyncedItem arg,
List<SyncedItem> children,
); );
} }
@ -197,9 +478,11 @@ class SyncDownloadStatusFamily extends Family<DownloadStream?> {
/// See also [SyncDownloadStatus]. /// See also [SyncDownloadStatus].
SyncDownloadStatusProvider call( SyncDownloadStatusProvider call(
SyncedItem arg, SyncedItem arg,
List<SyncedItem> children,
) { ) {
return SyncDownloadStatusProvider( return SyncDownloadStatusProvider(
arg, arg,
children,
); );
} }
@ -209,6 +492,7 @@ class SyncDownloadStatusFamily extends Family<DownloadStream?> {
) { ) {
return call( return call(
provider.arg, provider.arg,
provider.children,
); );
} }
@ -233,8 +517,11 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
/// See also [SyncDownloadStatus]. /// See also [SyncDownloadStatus].
SyncDownloadStatusProvider( SyncDownloadStatusProvider(
SyncedItem arg, SyncedItem arg,
List<SyncedItem> children,
) : this._internal( ) : this._internal(
() => SyncDownloadStatus()..arg = arg, () => SyncDownloadStatus()
..arg = arg
..children = children,
from: syncDownloadStatusProvider, from: syncDownloadStatusProvider,
name: r'syncDownloadStatusProvider', name: r'syncDownloadStatusProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -245,6 +532,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
allTransitiveDependencies: allTransitiveDependencies:
SyncDownloadStatusFamily._allTransitiveDependencies, SyncDownloadStatusFamily._allTransitiveDependencies,
arg: arg, arg: arg,
children: children,
); );
SyncDownloadStatusProvider._internal( SyncDownloadStatusProvider._internal(
@ -255,9 +543,11 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.arg, required this.arg,
required this.children,
}) : super.internal(); }) : super.internal();
final SyncedItem arg; final SyncedItem arg;
final List<SyncedItem> children;
@override @override
DownloadStream? runNotifierBuild( DownloadStream? runNotifierBuild(
@ -265,6 +555,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
) { ) {
return notifier.build( return notifier.build(
arg, arg,
children,
); );
} }
@ -273,13 +564,16 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SyncDownloadStatusProvider._internal( override: SyncDownloadStatusProvider._internal(
() => create()..arg = arg, () => create()
..arg = arg
..children = children,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
arg: arg, arg: arg,
children: children,
), ),
); );
} }
@ -292,13 +586,16 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SyncDownloadStatusProvider && other.arg == arg; return other is SyncDownloadStatusProvider &&
other.arg == arg &&
other.children == children;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode); hash = _SystemHash.combine(hash, arg.hashCode);
hash = _SystemHash.combine(hash, children.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@ -309,6 +606,9 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
mixin SyncDownloadStatusRef on AutoDisposeNotifierProviderRef<DownloadStream?> { mixin SyncDownloadStatusRef on AutoDisposeNotifierProviderRef<DownloadStream?> {
/// The parameter `arg` of this provider. /// The parameter `arg` of this provider.
SyncedItem get arg; SyncedItem get arg;
/// The parameter `children` of this provider.
List<SyncedItem> get children;
} }
class _SyncDownloadStatusProviderElement class _SyncDownloadStatusProviderElement
@ -318,161 +618,20 @@ class _SyncDownloadStatusProviderElement
@override @override
SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg; SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg;
@override
List<SyncedItem> get children =>
(origin as SyncDownloadStatusProvider).children;
} }
String _$syncStatusesHash() => r'f05ee53368d1de130714bba09132e08aba15bc44'; String _$syncSizeHash() => r'eeb6ab8dc1fdf5696c06e53f04a0e54ad68c6748';
abstract class _$SyncStatuses
extends BuildlessAutoDisposeAsyncNotifier<SyncStatus> {
late final SyncedItem arg;
FutureOr<SyncStatus> build(
SyncedItem arg,
);
}
/// See also [SyncStatuses].
@ProviderFor(SyncStatuses)
const syncStatusesProvider = SyncStatusesFamily();
/// See also [SyncStatuses].
class SyncStatusesFamily extends Family<AsyncValue<SyncStatus>> {
/// See also [SyncStatuses].
const SyncStatusesFamily();
/// See also [SyncStatuses].
SyncStatusesProvider call(
SyncedItem arg,
) {
return SyncStatusesProvider(
arg,
);
}
@override
SyncStatusesProvider getProviderOverride(
covariant SyncStatusesProvider provider,
) {
return call(
provider.arg,
);
}
static const Iterable<ProviderOrFamily>? _dependencies = null;
@override
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
@override
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
_allTransitiveDependencies;
@override
String? get name => r'syncStatusesProvider';
}
/// See also [SyncStatuses].
class SyncStatusesProvider
extends AutoDisposeAsyncNotifierProviderImpl<SyncStatuses, SyncStatus> {
/// See also [SyncStatuses].
SyncStatusesProvider(
SyncedItem arg,
) : this._internal(
() => SyncStatuses()..arg = arg,
from: syncStatusesProvider,
name: r'syncStatusesProvider',
debugGetCreateSourceHash:
const bool.fromEnvironment('dart.vm.product')
? null
: _$syncStatusesHash,
dependencies: SyncStatusesFamily._dependencies,
allTransitiveDependencies:
SyncStatusesFamily._allTransitiveDependencies,
arg: arg,
);
SyncStatusesProvider._internal(
super._createNotifier, {
required super.name,
required super.dependencies,
required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash,
required super.from,
required this.arg,
}) : super.internal();
final SyncedItem arg;
@override
FutureOr<SyncStatus> runNotifierBuild(
covariant SyncStatuses notifier,
) {
return notifier.build(
arg,
);
}
@override
Override overrideWith(SyncStatuses Function() create) {
return ProviderOverride(
origin: this,
override: SyncStatusesProvider._internal(
() => create()..arg = arg,
from: from,
name: null,
dependencies: null,
allTransitiveDependencies: null,
debugGetCreateSourceHash: null,
arg: arg,
),
);
}
@override
AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
createElement() {
return _SyncStatusesProviderElement(this);
}
@override
bool operator ==(Object other) {
return other is SyncStatusesProvider && other.arg == arg;
}
@override
int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode);
return _SystemHash.finish(hash);
}
}
@Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element
mixin SyncStatusesRef on AutoDisposeAsyncNotifierProviderRef<SyncStatus> {
/// The parameter `arg` of this provider.
SyncedItem get arg;
}
class _SyncStatusesProviderElement
extends AutoDisposeAsyncNotifierProviderElement<SyncStatuses, SyncStatus>
with SyncStatusesRef {
_SyncStatusesProviderElement(super.provider);
@override
SyncedItem get arg => (origin as SyncStatusesProvider).arg;
}
String _$syncSizeHash() => r'138702f2dd69ab28d142bab67ab4a497bb24f252';
abstract class _$SyncSize extends BuildlessAutoDisposeNotifier<int?> { abstract class _$SyncSize extends BuildlessAutoDisposeNotifier<int?> {
late final SyncedItem arg; late final SyncedItem arg;
late final List<SyncedItem>? children;
int? build( int? build(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
); );
} }
@ -488,9 +647,11 @@ class SyncSizeFamily extends Family<int?> {
/// See also [SyncSize]. /// See also [SyncSize].
SyncSizeProvider call( SyncSizeProvider call(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
) { ) {
return SyncSizeProvider( return SyncSizeProvider(
arg, arg,
children,
); );
} }
@ -500,6 +661,7 @@ class SyncSizeFamily extends Family<int?> {
) { ) {
return call( return call(
provider.arg, provider.arg,
provider.children,
); );
} }
@ -523,8 +685,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
/// See also [SyncSize]. /// See also [SyncSize].
SyncSizeProvider( SyncSizeProvider(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
) : this._internal( ) : this._internal(
() => SyncSize()..arg = arg, () => SyncSize()
..arg = arg
..children = children,
from: syncSizeProvider, from: syncSizeProvider,
name: r'syncSizeProvider', name: r'syncSizeProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -534,6 +699,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
dependencies: SyncSizeFamily._dependencies, dependencies: SyncSizeFamily._dependencies,
allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies, allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies,
arg: arg, arg: arg,
children: children,
); );
SyncSizeProvider._internal( SyncSizeProvider._internal(
@ -544,9 +710,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.arg, required this.arg,
required this.children,
}) : super.internal(); }) : super.internal();
final SyncedItem arg; final SyncedItem arg;
final List<SyncedItem>? children;
@override @override
int? runNotifierBuild( int? runNotifierBuild(
@ -554,6 +722,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
) { ) {
return notifier.build( return notifier.build(
arg, arg,
children,
); );
} }
@ -562,13 +731,16 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SyncSizeProvider._internal( override: SyncSizeProvider._internal(
() => create()..arg = arg, () => create()
..arg = arg
..children = children,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
arg: arg, arg: arg,
children: children,
), ),
); );
} }
@ -580,13 +752,16 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SyncSizeProvider && other.arg == arg; return other is SyncSizeProvider &&
other.arg == arg &&
other.children == children;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode); hash = _SystemHash.combine(hash, arg.hashCode);
hash = _SystemHash.combine(hash, children.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@ -597,6 +772,9 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
mixin SyncSizeRef on AutoDisposeNotifierProviderRef<int?> { mixin SyncSizeRef on AutoDisposeNotifierProviderRef<int?> {
/// The parameter `arg` of this provider. /// The parameter `arg` of this provider.
SyncedItem get arg; SyncedItem get arg;
/// The parameter `children` of this provider.
List<SyncedItem>? get children;
} }
class _SyncSizeProviderElement class _SyncSizeProviderElement
@ -606,6 +784,8 @@ class _SyncSizeProviderElement
@override @override
SyncedItem get arg => (origin as SyncSizeProvider).arg; SyncedItem get arg => (origin as SyncSizeProvider).arg;
@override
List<SyncedItem>? get children => (origin as SyncSizeProvider).children;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -8,10 +8,10 @@ import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:collection/collection.dart'; import 'package:collection/collection.dart';
import 'package:drift_db_viewer/drift_db_viewer.dart';
import 'package:file_picker/file_picker.dart'; import 'package:file_picker/file_picker.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:isar/isar.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart'; import 'package:path_provider/path_provider.dart';
@ -20,12 +20,14 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/chapters_model.dart'; import 'package:fladder/models/items/chapters_model.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/images_models.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/media_streams_model.dart';
import 'package:fladder/models/items/movie_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/series_model.dart';
import 'package:fladder/models/items/trick_play_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/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_item.dart';
import 'package:fladder/models/syncing/sync_settings_model.dart'; import 'package:fladder/models/syncing/sync_settings_model.dart';
import 'package:fladder/models/video_stream_model.dart'; import 'package:fladder/models/video_stream_model.dart';
@ -37,16 +39,27 @@ import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/localization_helper.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 syncProvider = StateNotifierProvider<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError());
final downloadTasksProvider = StateProvider.family<DownloadStream, String>((ref, id) => DownloadStream.empty()); final downloadTasksProvider = StateProvider.family<DownloadStream, String>((ref, id) => DownloadStream.empty());
class SyncNotifier extends StateNotifier<SyncSettingsModel> { class SyncNotifier extends StateNotifier<SyncSettingsModel> {
SyncNotifier(this.ref, this.isar, this.mobileDirectory) : super(SyncSettingsModel()) { SyncNotifier(this.ref, this.mobileDirectory) : super(SyncSettingsModel()) {
_init(); _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() { void _init() {
cleanupTemporaryFiles(); cleanupTemporaryFiles();
ref.listen( ref.listen(
@ -54,35 +67,29 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
(previous, next) { (previous, next) {
if (previous?.id != next?.id) { if (previous?.id != next?.id) {
if (next?.id != null) { if (next?.id != null) {
_initializeQueryStream(next?.id ?? ""); _initializeQueryStream();
} }
} }
}, },
); );
final userId = ref.read(userProvider)?.id; _initializeQueryStream();
if (userId != null) {
_initializeQueryStream(userId); migrateFromIsar();
}
} }
void _initializeQueryStream(String userId) { void _initializeQueryStream() async {
final userId = ref.read(userProvider)?.id;
if (userId == null) return;
_subscription?.cancel(); _subscription?.cancel();
final queryStream = getParentSyncItems final queryStream = db.getParentItems.watch();
?.userIdEqualTo(userId)
.watch()
.asyncMap((event) => event.map((e) => SyncedItem.fromIsar(e, syncPath ?? "")).toList());
final initItems = getParentSyncItems final initItems = await db.getParentItems.get();
?.userIdEqualTo(userId)
.findAll()
.mapIndexed((index, element) => SyncedItem.fromIsar(element, syncPath ?? ""))
.toList();
state = state.copyWith(items: initItems ?? []); state = state.copyWith(items: initItems);
_subscription = queryStream?.listen((items) { _subscription = queryStream.listen((items) {
state = state.copyWith(items: items); state = state.copyWith(items: items);
}); });
} }
@ -116,15 +123,8 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
} }
} }
final Ref ref;
final Isar? isar;
final Directory mobileDirectory;
final String subPath = "Synced";
StreamSubscription<List<SyncedItem>>? _subscription; StreamSubscription<List<SyncedItem>>? _subscription;
IsarCollection<String, ISyncedItem>? get syncedItems => isar?.iSyncedItems;
late final JellyService api = ref.read(jellyApiProvider); late final JellyService api = ref.read(jellyApiProvider);
String? get _savePath => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS) String? get _savePath => !kIsWeb && (Platform.isWindows || Platform.isLinux || Platform.isMacOS)
@ -150,9 +150,6 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
String? get syncPath => saveDirectory?.path; String? get syncPath => saveDirectory?.path;
QueryBuilder<ISyncedItem, ISyncedItem, QAfterFilterCondition>? get getParentSyncItems =>
syncedItems?.where().parentIdIsNull();
Future<int> get directorySize async { Future<int> get directorySize async {
if (saveDirectory == null) return 0; if (saveDirectory == null) return 0;
var files = await saveDirectory!.list(recursive: true).toList(); var files = await saveDirectory!.list(recursive: true).toList();
@ -167,59 +164,62 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
} }
Future<void> refresh() async { Future<void> refresh() async {
state = state.copyWith( state = state.copyWith(items: (await db.getParentItems.get()));
items: (await getParentSyncItems?.userIdEqualTo(ref.read(userProvider)?.id).findAllAsync())
?.map((e) => SyncedItem.fromIsar(e, syncPath ?? ""))
.toList() ??
[]);
} }
List<SyncedItem> getNestedChildren(SyncedItem item) { Future<List<SyncedItem>> getNestedChildren(SyncedItem item) async => db.getNestedChildren(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;
}
List<SyncedItem> getChildren(SyncedItem item) { Future<List<SyncedItem>> getChildren(SyncedItem root) async => await db.getChildren(root.id).get();
return (syncedItems?.where().parentIdEqualTo(item.id).sortBySortName().findAll())
?.map(
(e) => SyncedItem.fromIsar(e, syncPath ?? ""),
)
.toList() ??
[];
}
SyncedItem? getSyncedItem(ItemBaseModel? item) { Future<SyncedItem?> getSyncedItem(ItemBaseModel? item) async {
final id = item?.id; final id = item?.id;
if (id == null) return null; if (id == null) return null;
final newItem = syncedItems?.get(id); return await db.getItem(id).getSingleOrNull();
if (newItem == null) return null;
return SyncedItem.fromIsar(newItem, syncPath ?? "");
} }
SyncedItem? getParentItem(String id) { Future<SyncedItem?> getParentItem(String id) async => await db.getParent(id).getSingleOrNull();
ISyncedItem? newItem = syncedItems?.get(id);
while (newItem?.parentId != null) { Future<SyncedItem> refreshSyncItem(SyncedItem item) async {
newItem = syncedItems?.get(newItem!.parentId!); 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) { await db.insertMultipleEntries(newItems);
if (syncedItem == null) return null;
return syncedItem.createItemModel(ref); return parentItem;
} }
Future<void> addSyncItem(BuildContext? context, ItemBaseModel item) async { Future<void> addSyncItem(BuildContext? context, ItemBaseModel item) async {
@ -228,7 +228,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
if (saveDirectory == null) { if (saveDirectory == null) {
String? selectedDirectory = String? selectedDirectory =
await FilePicker.platform.getDirectoryPath(dialogTitle: context.localized.syncSelectDownloadsFolder); await FilePicker.platform.getDirectoryPath(dialogTitle: context.localized.syncSelectDownloadsFolder);
if (selectedDirectory?.isEmpty == true) { if (selectedDirectory?.isEmpty == true && context.mounted) {
fladderSnackbar(context, title: context.localized.syncNoFolderSetup); fladderSnackbar(context, title: context.localized.syncNoFolderSetup);
return; return;
} }
@ -238,22 +238,29 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
fladderSnackbar(context, title: context.localized.syncAddItemForSyncing(item.detailedName(context) ?? "Unknown")); fladderSnackbar(context, title: context.localized.syncAddItemForSyncing(item.detailedName(context) ?? "Unknown"));
final newSync = switch (item) { final newSync = switch (item) {
EpisodeModel episode => await syncSeries(item.parentBaseModel, episode: episode), EpisodeModel episode => await syncSeries(item.parentBaseModel, episode: episode),
SeasonModel season => await syncSeries(item.parentBaseModel, season: season),
SeriesModel series => await syncSeries(series), SeriesModel series => await syncSeries(series),
MovieModel movie => await syncMovie(movie), MovieModel movie => await syncMovie(movie),
_ => null _ => null
}; };
fladderSnackbar(context, if (context.mounted) {
title: newSync != null fladderSnackbar(context,
? context.localized.startedSyncingItem(item.detailedName(context) ?? "Unknown") title: newSync != null
: context.localized.unableToSyncItem(item.detailedName(context) ?? "Unknown")); ? context.localized.startedSyncingItem(item.detailedName(context) ?? "Unknown")
: context.localized.unableToSyncItem(item.detailedName(context) ?? "Unknown"));
}
return; return;
} }
void viewDatabase(BuildContext context) =>
Navigator.of(context, rootNavigator: true).push(MaterialPageRoute(builder: (context) => DriftDbViewer(db)));
Future<bool> removeSync(BuildContext context, SyncedItem? item) async { Future<bool> removeSync(BuildContext context, SyncedItem? item) async {
try { try {
if (item == null) return false; if (item == null) return false;
final nestedChildren = getNestedChildren(item); final nestedChildren = await getNestedChildren(item);
state = state.copyWith( state = state.copyWith(
items: state.items items: state.items
@ -266,13 +273,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!); await ref.read(backgroundDownloaderProvider).cancelTaskWithId(item.taskId!);
} }
final deleteFromDatabase = isar?.write((isar) => syncedItems?.deleteAll([...nestedChildren, item] await db.deleteAllItems([...nestedChildren, item]);
.map(
(e) => e.id,
)
.toList()));
if (deleteFromDatabase == 0) return false;
for (var i = 0; i < nestedChildren.length; i++) { for (var i = 0; i < nestedChildren.length; i++) {
final element = nestedChildren[i]; final element = nestedChildren[i];
@ -367,7 +368,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
if (data == null) return data; if (data == null) return data;
if (!itemPath.existsSync()) return data; if (!itemPath.existsSync()) return data;
if (data.isEmpty) return data; if (data.isEmpty) return data;
final saveDirectory = Directory(path.joinAll([itemPath.path, "Chapters"])); final saveDirectory = Directory(path.joinAll([itemPath.path, SyncedItem.chaptersPath]));
await saveDirectory.create(recursive: true); await saveDirectory.create(recursive: true);
@ -378,7 +379,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
if (response.bodyBytes.isEmpty) return null; if (response.bodyBytes.isEmpty) return null;
file.writeAsBytesSync(response.bodyBytes); file.writeAsBytesSync(response.bodyBytes);
return event.copyWith( return event.copyWith(
imageUrl: path.joinAll(["Chapters", fileName]), imageUrl: path.joinAll([SyncedItem.chaptersPath, fileName]),
); );
}).toList(); }).toList();
return saveChapters.nonNulls.toList(); return saveChapters.nonNulls.toList();
@ -394,12 +395,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
return data?.copyWith(path: fileName); return data?.copyWith(path: fileName);
} }
void updateItemSync(SyncedItem syncedItem) => Future<void> updateItem(SyncedItem syncedItem) async => db.insertItem(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<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem, DownloadTask? task) async { Future<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem, DownloadTask? task) async {
await syncedItem.deleteDatFiles(ref); await syncedItem.deleteDatFiles(ref);
@ -415,7 +411,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
return syncedItem; return syncedItem;
} }
Future<DownloadStream?> syncVideoFile(SyncedItem syncItem, bool skipDownload) async { Future<DownloadStream?> syncFile(SyncedItem syncItem, bool skipDownload) async {
cleanupTemporaryFiles(); cleanupTemporaryFiles();
final playbackResponse = await api.itemsItemIdPlaybackInfoPost( final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
@ -439,6 +435,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
final mediaSegments = (await api.mediaSegmentsGet(id: syncItem.id))?.body; final mediaSegments = (await api.mediaSegmentsGet(id: syncItem.id))?.body;
syncItem = syncItem.copyWith( syncItem = syncItem.copyWith(
fChapters: await saveChapterImages(item?.overview.chapters, directory) ?? [],
subtitles: subtitles, subtitles: subtitles,
fTrickPlayModel: trickPlayFile, fTrickPlayModel: trickPlayFile,
mediaSegments: mediaSegments, mediaSegments: mediaSegments,
@ -447,23 +444,24 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
await updateItem(syncItem); await updateItem(syncItem);
final currentTask = ref.read(downloadTasksProvider(syncItem.id)); final currentTask = ref.read(downloadTasksProvider(syncItem.id));
final user = ref.read(userProvider);
final downloadString = path.joinAll([ if (user == null) return null;
"${ref.read(userProvider)?.server}",
"Items", final downloadUrl = path.joinAll([user.server, "Items", syncItem.id, "Download"]);
"${syncItem.id}/Download?api_key=${ref.read(userProvider)?.credentials.token}"
]);
try { try {
if (!skipDownload && currentTask.task == null) { if (!skipDownload && currentTask.task == null) {
final downloadTask = DownloadTask( final downloadTask = DownloadTask(
url: Uri.parse(downloadString).toString(), url: Uri.parse(downloadUrl).toString(),
directory: syncItem.directory.path, directory: syncItem.directory.path,
filename: syncItem.videoFileName, filename: syncItem.videoFileName,
updates: Updates.statusAndProgress, updates: Updates.statusAndProgress,
baseDirectory: BaseDirectory.root, baseDirectory: BaseDirectory.root,
urlQueryParameters: {"api_key": user.credentials.token},
headers: user.credentials.header(ref),
requiresWiFi: ref.read(clientSettingsProvider.select((value) => value.requireWifi)), requiresWiFi: ref.read(clientSettingsProvider.select((value) => value.requireWifi)),
retries: 5, retries: 3,
allowPause: true, allowPause: true,
); );
@ -490,7 +488,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
(state) => state.copyWith(status: status), (state) => state.copyWith(status: status),
); );
if (status == TaskStatus.complete) { if (status == TaskStatus.complete || status == TaskStatus.canceled) {
ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => DownloadStream.empty()); ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => DownloadStream.empty());
} }
}, },
@ -508,7 +506,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
Future<void> clear() async { Future<void> clear() async {
await mainDirectory.delete(recursive: true); await mainDirectory.delete(recursive: true);
isar?.write((isar) => syncedItems?.clear()); await db.clearDatabase();
state = state.copyWith(items: []); state = state.copyWith(items: []);
} }
@ -522,6 +520,24 @@ extension SyncNotifierHelpers on SyncNotifier {
Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async { Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async {
final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref); final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref);
final existingSyncedItem = await getSyncedItem(item);
if (existingSyncedItem != null) return existingSyncedItem;
SyncedItem syncItem = await _syncItemData(parent, item, response);
if (parent == null) {
await db.insertItem(syncItem);
}
return syncItem.copyWith(
fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
syncing: false,
videoFileName: response.path?.split('/').lastOrNull ?? "",
);
}
Future<SyncedItem> _syncItemData(SyncedItem? parent, ItemBaseModel item, BaseItemDto response) async {
final Directory? parentDirectory = parent?.directory; final Directory? parentDirectory = parent?.directory;
final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id])); final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id]));
@ -542,30 +558,7 @@ extension SyncNotifierHelpers on SyncNotifier {
path: directory.path, path: directory.path,
userData: item.userData, userData: item.userData,
); );
return syncItem;
//Save item if parent so the user is aware.
if (parent == null) {
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
}
final origChapters = Chapter.chaptersFromInfo(item.id, response.chapters ?? [], ref);
return syncItem.copyWith(
fChapters: await saveChapterImages(origChapters, directory) ?? [],
fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
syncing: false,
videoFileName: response.path?.split('/').lastOrNull ?? "",
);
}
// Need to move the file after downloading on Android
Future<void> moveFile(DownloadTask downloadTask, SyncedItem syncItem) async {
final currentLocation = File(await downloadTask.filePath());
final wantedLocation = syncItem.videoFile;
if (currentLocation.path != wantedLocation.path) {
await currentLocation.copy(wantedLocation.path);
await currentLocation.delete();
}
} }
Future<SyncedItem?> syncMovie(ItemBaseModel item, {bool skipDownload = false}) async { Future<SyncedItem?> syncMovie(ItemBaseModel item, {bool skipDownload = false}) async {
@ -580,27 +573,28 @@ extension SyncNotifierHelpers on SyncNotifier {
if (!syncItem.directory.existsSync()) return null; if (!syncItem.directory.existsSync()) return null;
await syncVideoFile(syncItem, skipDownload); await db.insertItem(syncItem);
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath))); await syncFile(syncItem, skipDownload);
return syncItem; return syncItem;
} }
Future<SyncedItem?> syncSeries(SeriesModel item, {EpisodeModel? episode}) async { Future<SyncedItem?> syncSeries(SeriesModel item, {SeasonModel? season, EpisodeModel? episode}) async {
final response = await api.usersUserIdItemsItemIdGetBaseItem( final response = await api.usersUserIdItemsItemIdGetBaseItem(
itemId: item.id, itemId: item.id,
); );
List<SyncedItem> newItems = []; List<SyncedItem> newItems = [];
SyncedItem? itemToDownload; List<SyncedItem>? itemsToDownload = [];
SyncedItem seriesItem = await createSyncItem(response.bodyOrThrow); SyncedItem seriesItem = await createSyncItem(response.bodyOrThrow);
newItems.add(seriesItem); newItems.add(seriesItem);
if (!seriesItem.directory.existsSync()) return null; if (!seriesItem.directory.existsSync()) return null;
final seasonsResponse = await api.showsSeriesIdSeasonsGet( final seasonsResponse = await api.showsSeriesIdSeasonsGet(
seriesId: item.id,
isMissing: false, isMissing: false,
enableUserData: true, enableUserData: true,
fields: [ fields: [
@ -621,14 +615,13 @@ extension SyncNotifierHelpers on SyncNotifier {
ItemFields.chapters, ItemFields.chapters,
ItemFields.trickplay, ItemFields.trickplay,
], ],
seriesId: item.id,
); );
final seasons = seasonsResponse.body?.items ?? []; final seasons = seasonsResponse.body?.items ?? [];
for (var i = 0; i < seasons.length; i++) { for (var i = 0; i < seasons.length; i++) {
final season = seasons[i]; final newSeason = seasons[i];
final syncedSeason = await createSyncItem(season, parent: seriesItem); final syncedSeason = await createSyncItem(newSeason, parent: seriesItem);
newItems.add(syncedSeason); newItems.add(syncedSeason);
final episodesResponse = await api.showsSeriesIdEpisodesGet( final episodesResponse = await api.showsSeriesIdEpisodesGet(
isMissing: false, isMissing: false,
@ -651,30 +644,33 @@ extension SyncNotifierHelpers on SyncNotifier {
ItemFields.chapters, ItemFields.chapters,
ItemFields.trickplay, ItemFields.trickplay,
], ],
seasonId: season.id, seasonId: newSeason.id,
seriesId: seriesItem.id, seriesId: seriesItem.id,
); );
final episodes = episodesResponse.body?.items ?? []; final episodes = episodesResponse.body?.items ?? [];
for (var i = 0; i < episodes.length; i++) {
final item = episodes[i]; final episodeResults = await Future.wait(
final newEpisode = await createSyncItem(item, parent: syncedSeason); episodes.map((ep) async {
final newEpisode = await createSyncItem(ep, parent: syncedSeason);
return (ep, newEpisode);
}),
);
for (final (ep, newEpisode) in episodeResults) {
newItems.add(newEpisode); newItems.add(newEpisode);
if (episode?.id == item.id) { if (episode?.id == ep.id || newSeason.id == season?.id && !await newEpisode.videoFile.exists()) {
itemToDownload = newEpisode; itemsToDownload.add(newEpisode);
} }
} }
} }
isar?.write( await db.insertMultipleEntries(newItems);
(isar) => syncedItems?.putAll(newItems
.map(
(e) => ISyncedItem.fromSynced(e, syncPath ?? ""),
)
.toList()),
);
if (itemToDownload != null) { for (var i = 0; i < itemsToDownload.length; i++) {
await syncVideoFile(itemToDownload, false); final item = itemsToDownload[i];
//No need to await file sync happens in the background
syncFile(item, false);
} }
return seriesItem; return seriesItem;

View file

@ -71,22 +71,26 @@ final AutoRoute _dashboardRoute = CustomRoute(
page: DashboardRoute.page, page: DashboardRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
initial: true, initial: true,
maintainState: false,
path: 'dashboard', path: 'dashboard',
); );
final AutoRoute _favouritesRoute = CustomRoute( final AutoRoute _favouritesRoute = CustomRoute(
page: FavouritesRoute.page, page: FavouritesRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
maintainState: false,
path: 'favourites', path: 'favourites',
); );
final AutoRoute _syncedRoute = CustomRoute( final AutoRoute _syncedRoute = CustomRoute(
page: SyncedRoute.page, page: SyncedRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
maintainState: false,
path: 'synced', path: 'synced',
); );
final AutoRoute _librariesRoute = CustomRoute( final AutoRoute _librariesRoute = CustomRoute(
page: LibraryRoute.page, page: LibraryRoute.page,
transitionsBuilder: TransitionsBuilders.fadeIn, transitionsBuilder: TransitionsBuilders.fadeIn,
maintainState: false,
path: 'libraries', path: 'libraries',
); );

View file

@ -464,7 +464,7 @@ class SplashRouteArgs {
class SyncedRoute extends _i17.PageRouteInfo<SyncedRouteArgs> { class SyncedRoute extends _i17.PageRouteInfo<SyncedRouteArgs> {
SyncedRoute({ SyncedRoute({
_i22.ScrollController? navigationScrollController, _i22.ScrollController? navigationScrollController,
_i22.Key? key, _i19.Key? key,
List<_i17.PageRouteInfo>? children, List<_i17.PageRouteInfo>? children,
}) : super( }) : super(
SyncedRoute.name, SyncedRoute.name,
@ -498,7 +498,7 @@ class SyncedRouteArgs {
final _i22.ScrollController? navigationScrollController; final _i22.ScrollController? navigationScrollController;
final _i22.Key? key; final _i19.Key? key;
@override @override
String toString() { String toString() {

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/book_model.dart';
import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/images_models.dart';

View file

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@ -91,7 +92,7 @@ class HomeScreen extends ConsumerWidget {
action: () => e.navigate(context), action: () => e.navigate(context),
); );
case HomeTabs.sync: case HomeTabs.sync:
if (canDownload) { if (canDownload && !kIsWeb) {
return DestinationModel( return DestinationModel(
label: context.localized.navigationSync, label: context.localized.navigationSync,
icon: Icon(e.icon), icon: Icon(e.icon),

View file

@ -6,7 +6,10 @@ import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/images_models.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/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/theme.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
@ -196,6 +199,19 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
children: [ children: [
if (widget.item != null) ...[ 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(
builder: (context) { builder: (context) {
final newActions = widget.actions?.call(context); final newActions = widget.actions?.call(context);

View file

@ -10,6 +10,7 @@ void fladderSnackbar(
bool showCloseButton = false, bool showCloseButton = false,
Duration duration = const Duration(seconds: 3), Duration duration = const Duration(seconds: 3),
}) { }) {
if (!context.mounted) return;
ScaffoldMessenger.of(context).showSnackBar(SnackBar( ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text( content: Text(
title, title,

View file

@ -4,7 +4,6 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:flutter_widget_from_html/flutter_widget_from_html.dart'; import 'package:flutter_widget_from_html/flutter_widget_from_html.dart';
import 'package:fladder/models/items/episode_model.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/details_screens/components/media_stream_information.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -31,7 +30,7 @@ class NextUpEpisode extends ConsumerWidget {
Opacity( Opacity(
opacity: 0.75, opacity: 0.75,
child: SelectableText( child: SelectableText(
"${context.localized.season(1)} ${nextEpisode.season} - ${context.localized.episode(1)} ${nextEpisode.episode}", nextEpisode.seasonEpisodeLabelFull(context),
style: Theme.of(context).textTheme.titleMedium, style: Theme.of(context).textTheme.titleMedium,
), ),
), ),
@ -42,13 +41,11 @@ class NextUpEpisode extends ConsumerWidget {
const SizedBox(height: 16), const SizedBox(height: 16),
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(nextEpisode);
if (constraints.maxWidth < 550) { if (constraints.maxWidth < 550) {
return Column( return Column(
children: [ children: [
EpisodePoster( EpisodePoster(
episode: nextEpisode, episode: nextEpisode,
syncedItem: syncedItem,
showLabel: false, showLabel: false,
onTap: () => nextEpisode.navigateTo(context), onTap: () => nextEpisode.navigateTo(context),
actions: const [], actions: const [],
@ -71,7 +68,6 @@ class NextUpEpisode extends ConsumerWidget {
maxWidth: MediaQuery.of(context).size.width / 2), maxWidth: MediaQuery.of(context).size.width / 2),
child: EpisodePoster( child: EpisodePoster(
episode: nextEpisode, episode: nextEpisode,
syncedItem: syncedItem,
showLabel: false, showLabel: false,
onTap: () => nextEpisode.navigateTo(context), onTap: () => nextEpisode.navigateTo(context),
actions: const [], actions: const [],

View file

@ -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: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/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.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/item_base_model/item_base_model_extensions.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.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 { enum EpisodeDetailsViewType {
list(icon: IconsaxPlusBold.grid_6), list(icon: IconsaxPlusBold.grid_6),
@ -48,14 +48,12 @@ class EpisodeDetailsList extends ConsumerWidget {
itemCount: episodes.length, itemCount: episodes.length,
itemBuilder: (context, index) { itemBuilder: (context, index) {
final episode = episodes[index]; final episode = episodes[index];
final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(episode);
List<Widget> children = [ List<Widget> children = [
Flexible( Flexible(
flex: 1, flex: 1,
child: EpisodePoster( child: EpisodePoster(
episode: episode, episode: episode,
showLabel: false, showLabel: false,
syncedItem: syncedItem,
actions: episode.generateActions(context, ref), actions: episode.generateActions(context, ref),
onTap: () => episode.navigateTo(context), onTap: () => episode.navigateTo(context),
isCurrentEpisode: false, isCurrentEpisode: false,

View file

@ -3,10 +3,8 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.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/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.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/shared/flat_button.dart';
import 'package:fladder/screens/syncing/sync_button.dart'; import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
@ -88,11 +86,9 @@ class _EpisodePosterState extends ConsumerState<EpisodePosters> {
itemBuilder: (context, index) { itemBuilder: (context, index) {
final episode = episodes[index]; final episode = episodes[index];
final isCurrentEpisode = index == indexOfCurrent; final isCurrentEpisode = index == indexOfCurrent;
final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(episode);
return EpisodePoster( return EpisodePoster(
episode: episode, episode: episode,
blur: allPlayed ? false : indexOfCurrent < index, blur: allPlayed ? false : indexOfCurrent < index,
syncedItem: syncedItem,
onTap: widget.onEpisodeTap != null onTap: widget.onEpisodeTap != null
? () { ? () {
widget.onEpisodeTap?.call( widget.onEpisodeTap?.call(
@ -130,7 +126,6 @@ class _EpisodePosterState extends ConsumerState<EpisodePosters> {
class EpisodePoster extends ConsumerWidget { class EpisodePoster extends ConsumerWidget {
final EpisodeModel episode; final EpisodeModel episode;
final SyncedItem? syncedItem;
final bool showLabel; final bool showLabel;
final Function()? onTap; final Function()? onTap;
final Function()? onLongPress; final Function()? onLongPress;
@ -141,7 +136,6 @@ class EpisodePoster extends ConsumerWidget {
const EpisodePoster({ const EpisodePoster({
super.key, super.key,
required this.episode, required this.episode,
this.syncedItem,
this.showLabel = true, this.showLabel = true,
this.onTap, this.onTap,
this.onLongPress, this.onLongPress,
@ -156,7 +150,6 @@ class EpisodePoster extends ConsumerWidget {
color: Theme.of(context).colorScheme.surfaceContainerHighest, color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: const Icon(Icons.local_movies_outlined), child: const Icon(Icons.local_movies_outlined),
); );
final SyncedItem? iSyncedItem = syncedItem;
bool episodeAvailable = episode.status == EpisodeStatus.available; bool episodeAvailable = episode.status == EpisodeStatus.available;
return AspectRatio( return AspectRatio(
aspectRatio: 1.76, aspectRatio: 1.76,
@ -203,15 +196,18 @@ class EpisodePoster extends ConsumerWidget {
child: Row( child: Row(
mainAxisAlignment: MainAxisAlignment.end, mainAxisAlignment: MainAxisAlignment.end,
children: [ children: [
if (iSyncedItem != null) ref.watch(syncedItemProvider(episode)).when(
Consumer(builder: (context, ref, child) { error: (error, stackTrace) => const SizedBox.shrink(),
final SyncStatus syncStatus = data: (syncedItem) {
ref.watch(syncStatusesProvider(iSyncedItem)).value ?? SyncStatus.partially; if (syncedItem == null) {
return StatusCard( return const SizedBox.shrink();
color: syncStatus.color, }
child: SyncButton(item: episode, syncedItem: syncedItem), return StatusCard(
); child: SyncButton(item: episode, syncedItem: syncedItem),
}), );
},
loading: () => const SizedBox.shrink(),
),
if (episode.userData.isFavourite) if (episode.userData.isFavourite)
const StatusCard( const StatusCard(
color: Colors.red, color: Colors.red,
@ -259,7 +255,7 @@ class EpisodePoster extends ConsumerWidget {
child: Align( child: Align(
alignment: Alignment.bottomRight, alignment: Alignment.bottomRight,
child: PopupMenuButton( child: PopupMenuButton(
tooltip: "Options", tooltip: context.localized.options,
icon: const Icon( icon: const Icon(
Icons.more_vert, Icons.more_vert,
color: Colors.white, color: Colors.white,

View file

@ -19,6 +19,7 @@ class PosterWidget extends ConsumerWidget {
final int maxLines; final int maxLines;
final double? aspectRatio; final double? aspectRatio;
final bool inlineTitle; final bool inlineTitle;
final bool underTitle;
final Set<ItemActions> excludeActions; final Set<ItemActions> excludeActions;
final List<ItemAction> otherActions; final List<ItemAction> otherActions;
final Function(String id, UserData? newData)? onUserDataChanged; final Function(String id, UserData? newData)? onUserDataChanged;
@ -33,6 +34,7 @@ class PosterWidget extends ConsumerWidget {
this.heroTag, this.heroTag,
this.aspectRatio, this.aspectRatio,
this.inlineTitle = false, this.inlineTitle = false,
this.underTitle = true,
this.excludeActions = const {}, this.excludeActions = const {},
this.otherActions = const [], this.otherActions = const [],
this.onUserDataChanged, this.onUserDataChanged,
@ -64,7 +66,7 @@ class PosterWidget extends ConsumerWidget {
onPressed: onPressed, onPressed: onPressed,
), ),
), ),
if (!inlineTitle) if (!inlineTitle && underTitle)
Column( Column(
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.stretch, crossAxisAlignment: CrossAxisAlignment.stretch,

View file

@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/disable_keypad_focus.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
@ -97,30 +99,48 @@ class SeasonPoster extends ConsumerWidget {
alignment: Alignment.topLeft, alignment: Alignment.topLeft,
child: placeHolder(season.name), child: placeHolder(season.name),
), ),
if (season.userData.unPlayedItemCount != 0) Align(
Align( alignment: Alignment.topRight,
alignment: Alignment.topRight, child: Row(
child: StatusCard( mainAxisAlignment: MainAxisAlignment.end,
color: Theme.of(context).colorScheme.primary, crossAxisAlignment: CrossAxisAlignment.start,
useFittedBox: true, children: [
child: Center( ref.watch(syncedItemProvider(season)).when(
child: Text( error: (error, stackTrace) => const SizedBox.shrink(),
season.userData.unPlayedItemCount.toString(), data: (syncedItem) {
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14), if (syncedItem == null) {
return const SizedBox.shrink();
}
return StatusCard(
child: SyncButton(item: season, syncedItem: syncedItem),
);
},
loading: () => const SizedBox.shrink(),
),
if (season.userData.unPlayedItemCount != 0)
StatusCard(
color: Theme.of(context).colorScheme.primary,
useFittedBox: true,
child: Center(
child: Text(
season.userData.unPlayedItemCount.toString(),
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14),
),
),
)
else
Align(
alignment: Alignment.topRight,
child: StatusCard(
color: Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.check_rounded,
),
),
), ),
), ],
),
)
else
Align(
alignment: Alignment.topRight,
child: StatusCard(
color: Theme.of(context).colorScheme.primary,
child: const Icon(
Icons.check_rounded,
),
),
), ),
),
LayoutBuilder( LayoutBuilder(
builder: (context, constraints) { builder: (context, constraints) {
return FlatButton( return FlatButton(
@ -134,7 +154,7 @@ class SeasonPoster extends ConsumerWidget {
items: season.generateActions(context, ref).popupMenuItems(useIcons: true)); items: season.generateActions(context, ref).popupMenuItems(useIcons: true));
}, },
onTap: () => onSeasonPressed?.call(season), onTap: () => onSeasonPressed?.call(season),
onLongPress: AdaptiveLayout.of(context).inputDevice != InputDevice.touch onLongPress: AdaptiveLayout.of(context).inputDevice == InputDevice.touch
? () { ? () {
showBottomSheetPill( showBottomSheetPill(
context: context, context: context,

View file

@ -1,71 +1,51 @@
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncButton extends ConsumerStatefulWidget { class SyncButton extends ConsumerWidget {
final ItemBaseModel item; final ItemBaseModel item;
final SyncedItem? syncedItem; final SyncedItem syncedItem;
const SyncButton({required this.item, required this.syncedItem, super.key}); const SyncButton({required this.item, required this.syncedItem, super.key});
@override @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> { return Stack(
@override alignment: Alignment.center,
Widget build(BuildContext context) { children: [
final syncedItem = widget.syncedItem; Icon(
final status = syncedItem != null ? ref.watch(syncStatusesProvider(syncedItem)).value : null; status == TaskStatus.notFound
final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem)) : null; ? (progress > 0 ? IconsaxPlusLinear.arrow_down_1 : IconsaxPlusLinear.more_circle)
return Stack( : status.icon,
alignment: Alignment.center, color: status.color(context),
children: [ size: status == TaskStatus.running && progress > 0 ? 16 : null,
InkWell( ),
onTap: syncedItem != null SizedBox.fromSize(
? () => showSyncItemDetails(context, syncedItem, ref)
: () => showDefaultActionDialog(
context,
'Sync ${widget.item.detailedName}?',
null,
(context) async {
await ref.read(syncProvider.notifier).addSyncItem(context, widget.item);
Navigator.of(context).pop();
},
"Sync",
(context) => Navigator.of(context).pop(),
"Cancel",
),
child: Icon(
syncedItem != null
? status == SyncStatus.partially
? (progress?.progress ?? 0) > 0
? IconsaxPlusLinear.arrow_down
: IconsaxPlusLinear.more_circle
: IconsaxPlusLinear.tick_circle
: IconsaxPlusLinear.arrow_down_2,
color: status?.color,
size: (progress?.progress ?? 0) > 0 ? 16 : null,
),
),
if ((progress?.progress ?? 0) > 0)
IgnorePointer(
child: SizedBox.fromSize(
size: const Size.fromRadius(10), size: const Size.fromRadius(10),
child: CircularProgressIndicator( child: CircularProgressIndicator(
strokeCap: StrokeCap.round, strokeCap: StrokeCap.round,
strokeWidth: 2, strokeWidth: 1.5,
color: status?.color, color: status.color(context),
value: progress?.progress, value: status == TaskStatus.running ? progress.clamp(0.0, 1.0) : 0,
), ),
), ),
) ],
], );
},
); );
} }
} }

View file

@ -1,11 +1,11 @@
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/screens/syncing/widgets/synced_season_poster.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/screens/syncing/widgets/synced_season_poster.dart';
import 'widgets/synced_episode_item.dart'; import 'widgets/synced_episode_item.dart';
@ -25,39 +25,31 @@ class _ChildSyncWidgetState extends ConsumerState<ChildSyncWidget> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); final baseItem = syncedItem.itemModel;
final hasFile = syncedItem.videoFile.existsSync();
if (baseItem == null) { if (baseItem == null) {
return Container(); return Container();
} }
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 6), padding: const EdgeInsets.symmetric(vertical: 6),
child: Card( child: Card(
child: InkWell( child: Padding(
onTap: () { padding: const EdgeInsets.all(8.0),
Navigator.of(context).pop(); child: Row(
baseItem.navigateTo(context); children: [
}, Flexible(
child: Padding( child: switch (baseItem) {
padding: const EdgeInsets.all(8.0), SeasonModel season => SyncedSeasonPoster(
child: Row( syncedItem: syncedItem,
children: [ season: season,
Flexible( ),
child: switch (baseItem) { EpisodeModel episode => SyncedEpisodeItem(
SeasonModel season => SyncedSeasonPoster( episode: episode,
syncedItem: syncedItem, syncedItem: syncedItem,
season: season, ),
), _ => Container(),
EpisodeModel episode => SyncedEpisodeItem( },
episode: episode, ),
syncedItem: syncedItem, ],
hasFile: hasFile,
),
_ => Container(),
},
),
],
),
), ),
), ),
), ),

View file

@ -1,13 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/models/syncing/sync_item.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/adaptive_dialog.dart'; import 'package:fladder/screens/shared/adaptive_dialog.dart';
@ -15,26 +13,29 @@ import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/media/poster_widget.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart';
import 'package:fladder/screens/syncing/sync_child_item.dart'; import 'package:fladder/screens/syncing/sync_child_item.dart';
import 'package:fladder/screens/syncing/sync_widgets.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_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart'; import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.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/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/util/size_formatting.dart'; import 'package:fladder/util/size_formatting.dart';
import 'package:fladder/widgets/shared/alert_content.dart'; import 'package:fladder/widgets/shared/alert_content.dart';
import 'package:fladder/widgets/shared/icon_button_await.dart'; import 'package:fladder/widgets/shared/icon_button_await.dart';
import 'package:fladder/widgets/shared/pull_to_refresh.dart';
Future<void> showSyncItemDetails( Future<void> showSyncItemDetails(
BuildContext context, BuildContext context,
SyncedItem syncItem, SyncedItem syncItem,
WidgetRef ref, WidgetRef ref,
) { ) async {
return showDialogAdaptive( await showDialogAdaptive(
context: context, context: context,
builder: (context) => SyncItemDetails( builder: (context) => SyncItemDetails(
syncItem: syncItem, syncItem: syncItem,
), ),
); );
context.refreshData();
} }
class SyncItemDetails extends ConsumerStatefulWidget { class SyncItemDetails extends ConsumerStatefulWidget {
@ -50,200 +51,196 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); final baseItem = syncedItem.itemModel;
final hasFile = syncedItem.videoFile.existsSync(); final hasFile = syncedItem.videoFile.existsSync();
final syncChildren = ref.read(syncProvider.notifier).getChildren(syncedItem); final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id));
final downloadTask = ref.read(downloadTasksProvider(syncedItem.id)); final syncedChildren = ref.watch(syncedChildrenProvider(syncedItem));
final nestedChildren = ref.watch(syncedNestedChildrenProvider(syncedItem));
return SyncStatusOverlay( return PullToRefresh(
refreshOnStart: false,
onRefresh: () async {
final newItem = await ref.read(syncProvider.notifier).refreshSyncItem(syncedItem);
setState(() {
syncedItem = newItem;
});
},
child: SyncStatusOverlay(
syncedItem: syncedItem, syncedItem: syncedItem,
child: ActionContent( child: switch (syncedChildren) {
title: Row( AsyncValue<List<SyncedItem>>(value: final children) => ActionContent(
mainAxisAlignment: MainAxisAlignment.spaceBetween, title: Row(
children: [ mainAxisAlignment: MainAxisAlignment.spaceBetween,
Card( children: [
elevation: 1, Card(
child: Padding( elevation: 1,
padding: const EdgeInsets.all(12.0), child: Padding(
child: Text(baseItem?.type.label(context) ?? ""), padding: const EdgeInsets.all(12.0),
)), child: Text(baseItem?.type.label(context) ?? ""),
Text( )),
context.localized.navigationSync, Text(
style: Theme.of(context).textTheme.titleMedium, context.localized.navigationSync,
), style: Theme.of(context).textTheme.titleMedium,
IconButton(
onPressed: () => Navigator.pop(context),
icon: const Icon(IconsaxPlusBold.close_circle),
)
],
),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
if (baseItem != null) ...{
Row(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
SizedBox(
height: (AdaptiveLayout.poster(context).size *
ref.watch(clientSettingsProvider.select((value) => value.posterSize))) *
0.6,
child: IgnorePointer(
child: PosterWidget(
aspectRatio: 0.7,
poster: baseItem,
inlineTitle: true,
),
),
),
Expanded(
child: SyncProgressBuilder(
item: syncedItem,
builder: (context, combinedStream) {
return Row(
children: [
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
baseItem.detailedName(context) ?? "",
style: Theme.of(context).textTheme.titleMedium,
),
SyncSubtitle(syncItem: syncedItem),
SyncLabel(
label: context.localized
.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially,
),
].addInBetween(const SizedBox(height: 8)),
),
),
if (combinedStream?.task != null) ...{
if (combinedStream?.status != TaskStatus.paused)
IconButton(
onPressed: () =>
ref.read(backgroundDownloaderProvider).pause(combinedStream!.task!),
icon: const Icon(IconsaxPlusBold.pause),
),
if (combinedStream?.status == TaskStatus.paused) ...[
IconButton(
onPressed: () =>
ref.read(backgroundDownloaderProvider).resume(combinedStream!.task!),
icon: const Icon(IconsaxPlusBold.play),
),
IconButton(
onPressed: () => ref
.read(syncProvider.notifier)
.deleteFullSyncFiles(syncedItem, combinedStream?.task),
icon: const Icon(IconsaxPlusBold.stop),
),
],
const SizedBox(width: 16)
},
if (combinedStream != null && combinedStream.hasDownload)
SizedBox.fromSize(
size: const Size.fromRadius(35),
child: Stack(
fit: StackFit.expand,
alignment: Alignment.center,
children: [
CircularProgressIndicator(
value: combinedStream.progress,
strokeWidth: 8,
backgroundColor: Theme.of(context).colorScheme.surface.withValues(alpha: 0.5),
strokeCap: StrokeCap.round,
color: combinedStream.status.color(context),
),
Center(child: Text("${((combinedStream.progress) * 100).toStringAsFixed(0)}%"))
],
)),
],
);
},
),
),
if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile)
IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false),
icon: const Icon(IconsaxPlusLinear.cloud_change),
)
else if (hasFile)
IconButtonAwait(
color: Theme.of(context).colorScheme.error,
onPressed: () {
showDefaultAlertDialog(
context,
context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataDesc,
(context) {
ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
Navigator.of(context).pop();
},
context.localized.delete,
(context) => Navigator.of(context).pop(),
context.localized.cancel,
);
},
icon: const Icon(IconsaxPlusLinear.trash),
),
].addInBetween(const SizedBox(width: 16)),
),
},
const Divider(),
if (syncChildren.isNotEmpty == true)
Flexible(
child: ListView(
shrinkWrap: true,
children: [
...syncChildren.map(
(e) => ChildSyncWidget(syncedChild: e),
),
],
), ),
), IconButton(
], onPressed: () => Navigator.pop(context),
), icon: const Icon(IconsaxPlusBold.close_circle),
actions: [ )
if (baseItem is! EpisodeModel) ],
ElevatedButton( ),
style: ElevatedButton.styleFrom( child: ListView(
backgroundColor: Theme.of(context).colorScheme.errorContainer, shrinkWrap: true,
foregroundColor: Theme.of(context).colorScheme.onErrorContainer, children: [
iconColor: Theme.of(context).colorScheme.onErrorContainer, if (baseItem != null) ...{
), Row(
onPressed: () { mainAxisSize: MainAxisSize.min,
showDefaultAlertDialog( crossAxisAlignment: CrossAxisAlignment.center,
context, spacing: 16,
context.localized.syncDeleteItemTitle, children: [
context.localized.syncDeleteItemDesc(baseItem?.detailedName(context) ?? ""), SizedBox(
(context) async { height: (AdaptiveLayout.poster(context).size *
await ref.read(syncProvider.notifier).removeSync(context, syncedItem); ref.watch(clientSettingsProvider.select((value) => value.posterSize))) *
Navigator.pop(context); 0.6,
Navigator.pop(context); 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, child: Text(context.localized.delete),
(context) => Navigator.pop(context), )
context.localized.cancel, else if (baseItem?.parentBaseModel != null)
); ElevatedButton(
}, onPressed: () async {
child: Text(context.localized.delete), final parentItem = await ref.read(syncProvider.notifier).getSyncedItem(baseItem!.parentBaseModel);
) setState(() {
else if (syncedItem.parentId != null) if (parentItem != null) {
ElevatedButton( syncedItem = parentItem;
onPressed: () { }
final parentItem = ref.read(syncProvider.notifier).getParentItem(syncedItem.parentId!); });
setState(() { },
if (parentItem != null) { child: Text(context.localized.syncOpenParent),
syncedItem = parentItem; )
} ],
}); ),
}, },
child: Text(context.localized.syncOpenParent), ),
) );
],
));
} }
} }

View file

@ -1,5 +1,6 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.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_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart'; import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart';
import 'package:fladder/util/fladder_image.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/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart'; import 'package:fladder/util/size_formatting.dart';
@ -28,7 +28,7 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final syncedItem = widget.syncedItem; final syncedItem = widget.syncedItem;
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); final baseItem = syncedItem.itemModel;
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: SyncStatusOverlay( child: SyncStatusOverlay(
@ -66,82 +66,99 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
context.localized.cancel); context.localized.cancel);
return false; return false;
}, },
child: LayoutBuilder(builder: (context, constraints) { child: LayoutBuilder(
return IntrinsicHeight( builder: (context, constraints) {
child: InkWell( return IntrinsicHeight(
onTap: () => baseItem?.navigateTo(context), child: InkWell(
child: Padding( onTap: () => baseItem?.navigateTo(context),
padding: const EdgeInsets.all(8.0), child: Padding(
child: Row( padding: const EdgeInsets.all(8.0),
mainAxisSize: MainAxisSize.min, child: Row(
children: [ mainAxisSize: MainAxisSize.min,
ConstrainedBox( spacing: 16,
constraints: BoxConstraints(maxHeight: 125, maxWidth: constraints.maxWidth * 0.2), children: [
child: Card( ConstrainedBox(
child: AspectRatio( constraints: BoxConstraints(maxHeight: 125, maxWidth: constraints.maxWidth * 0.2),
aspectRatio: baseItem?.primaryRatio ?? 1.0, child: Card(
child: FladderImage( child: AspectRatio(
image: baseItem?.getPosters?.primary, aspectRatio: baseItem?.primaryRatio ?? 1.0,
fit: BoxFit.cover, child: FladderImage(
)), image: baseItem?.getPosters?.primary,
), fit: BoxFit.cover,
), )),
Expanded(
child: SyncProgressBuilder(
item: syncedItem,
builder: (context, combinedStream) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Flexible(
child: Text(
baseItem?.detailedName(context) ?? "",
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
Flexible(
child: SyncSubtitle(syncItem: syncedItem),
),
Flexible(
child: SyncLabel(
label: context.localized
.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially,
),
),
if (combinedStream != null && combinedStream.hasDownload == true)
SyncProgressBar(item: syncedItem, task: combinedStream)
].addInBetween(const SizedBox(height: 4)),
);
},
),
),
Column(
crossAxisAlignment: CrossAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Card(
elevation: 0,
shadowColor: Colors.transparent,
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Text(baseItem != null ? baseItem.type.label(context) : ""),
)),
IconButton(
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
icon: const Icon(IconsaxPlusLinear.more_square),
), ),
], ),
), Expanded(
].addInBetween(const SizedBox(width: 16)), 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),
),
],
),
],
),
), ),
), ),
), );
); },
}), ),
), ),
), ),
), ),

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/episode_model.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
@ -12,28 +12,34 @@ import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/background_download_provider.dart'; import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
const _cancellableStatuses = {
TaskStatus.canceled,
TaskStatus.failed,
TaskStatus.enqueued,
TaskStatus.waitingToRetry,
};
class SyncLabel extends ConsumerWidget { class SyncLabel extends ConsumerWidget {
final String? label; final String? label;
final SyncStatus status; final TaskStatus status;
const SyncLabel({this.label, required this.status, super.key}); const SyncLabel({this.label, required this.status, super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Container( return Container(
decoration: BoxDecoration( decoration: BoxDecoration(
color: status.color.withValues(alpha: 0.15), color: status.color(context).withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(10), borderRadius: BorderRadius.circular(10),
), ),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Text( child: Text(
label ?? status.label(context), label ?? status.name(context),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: status.color, color: status.color(context),
), ),
), ),
), ),
@ -61,6 +67,7 @@ class SyncProgressBar extends ConsumerWidget {
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
spacing: 8,
children: [ children: [
Flexible( Flexible(
child: LinearProgressIndicator( child: LinearProgressIndicator(
@ -72,23 +79,29 @@ class SyncProgressBar extends ConsumerWidget {
), ),
Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")), Opacity(opacity: 0.75, child: Text("${(downloadProgress * 100).toStringAsFixed(0)}%")),
if (downloadTask != null) ...{ if (downloadTask != null) ...{
if (downloadStatus != TaskStatus.paused) if (downloadStatus != TaskStatus.paused && downloadStatus != TaskStatus.enqueued)
IconButton( IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).pause(downloadTask), onPressed: () => ref.read(backgroundDownloaderProvider).pause(downloadTask),
icon: const Icon(IconsaxPlusBold.pause), icon: const Icon(IconsaxPlusBold.pause),
),
if (downloadStatus == TaskStatus.paused) ...[
IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask),
icon: const Icon(IconsaxPlusBold.play),
),
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
icon: const Icon(IconsaxPlusBold.stop),
) )
],
if (_cancellableStatuses.contains(downloadStatus)) ...[
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
icon: const Icon(IconsaxPlusBold.stop),
),
],
}, },
if (downloadStatus == TaskStatus.paused && downloadTask != null) ...[ ],
IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask),
icon: const Icon(IconsaxPlusBold.play),
),
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
icon: const Icon(IconsaxPlusBold.stop),
)
],
].addInBetween(const SizedBox(width: 8)),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
], ],
@ -98,41 +111,55 @@ class SyncProgressBar extends ConsumerWidget {
class SyncSubtitle extends ConsumerWidget { class SyncSubtitle extends ConsumerWidget {
final SyncedItem syncItem; final SyncedItem syncItem;
final List<SyncedItem> children;
const SyncSubtitle({ const SyncSubtitle({
required this.syncItem, required this.syncItem,
this.children = const [],
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncItem); final baseItem = syncItem.itemModel;
final children = syncItem.nestedChildren(ref); final syncStatus = ref
final syncStatus = ref.watch(syncStatusesProvider(syncItem)).value ?? SyncStatus.partially; .watch(syncDownloadStatusProvider(syncItem, children).select((value) => value?.status ?? TaskStatus.notFound));
return Container( return Container(
decoration: decoration: BoxDecoration(
BoxDecoration(color: syncStatus.color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)), color: syncStatus.color(context).withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)),
child: Material( child: Material(
color: const Color.fromARGB(0, 208, 130, 130), color: const Color.fromARGB(0, 208, 130, 130),
textStyle: textStyle: Theme.of(context)
Theme.of(context).textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.bold, color: syncStatus.color), .textTheme
.bodyMedium
?.copyWith(fontWeight: FontWeight.bold, color: syncStatus.color(context)),
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: switch (baseItem) { child: switch (baseItem) {
SeriesModel _ => Builder( SeasonModel _ => Builder(
builder: (context) { builder: (context) {
final itemBaseModels = children.map((e) => ref.read(syncProvider.notifier).getItem(e)); final itemBaseModels = children.map((e) => e.itemModel);
final seriesItemsSyncLeft = children.where((element) => element.taskId != null).length;
final seasons = itemBaseModels.whereType<SeasonModel>().length;
final episodes = itemBaseModels.whereType<EpisodeModel>().length; final episodes = itemBaseModels.whereType<EpisodeModel>().length;
return Text( return Text(
[ [
"${context.localized.season(seasons)}: $seasons", "${context.localized.episode(2)}: $episodes | ${context.localized.syncStatusSynced}: ${children.where((element) => element.videoFile.existsSync()).length}"
"${context.localized.episode(seasons)}: $episodes | ${context.localized.sync}: ${children.where((element) => element.videoFile.existsSync()).length}${seriesItemsSyncLeft > 0 ? " | Syncing: $seriesItemsSyncLeft" : ""}"
].join('\n'), ].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)),
}, },
), ),
), ),

View file

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart'; import 'package:auto_route/auto_route.dart';
@ -52,6 +53,30 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
) )
else else
const DefaultSliverTopBadding(), 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) ...[ if (items.isNotEmpty) ...[
SliverToBoxAdapter( SliverToBoxAdapter(
child: Padding( child: Padding(

View file

@ -0,0 +1,200 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/refresh_state.dart';
import 'package:fladder/widgets/shared/filled_button_await.dart';
class SyncOptionsButton extends ConsumerWidget {
final SyncedItem syncedItem;
final List<SyncedItem> children;
const SyncOptionsButton({
required this.syncedItem,
required this.children,
super.key,
});
@override
Widget build(BuildContext context, WidgetRef ref) {
return PopupMenuButton(
itemBuilder: (context) {
final unSyncedChildren = children.where((element) {
final hasDownload = ref.read(syncDownloadStatusProvider(element, []));
return element.hasVideoFile && !element.videoFile.existsSync() && hasDownload?.status == TaskStatus.notFound;
}).toList();
final syncedChildren =
children.where((element) => element.hasVideoFile && element.videoFile.existsSync()).toList();
final syncTasks = children
.map((element) {
final task = ref.read(syncDownloadStatusProvider(element, []));
if (task?.status != TaskStatus.notFound) {
return task;
} else {
return null;
}
})
.nonNulls
.toList();
final runningTasks = syncTasks.where((element) => element.status == TaskStatus.running).toList();
final enqueuedTasks = syncTasks.where((element) => element.status == TaskStatus.enqueued).toList();
final pausedTasks = syncTasks.where((element) => element.status == TaskStatus.paused).toList();
return <PopupMenuEntry>[
PopupMenuItem(
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.refresh_2),
Text(context.localized.refreshMetadata),
],
),
onTap: () => context.refreshData(),
),
if (children.isNotEmpty) ...[
const PopupMenuDivider(),
PopupMenuItem(
enabled: unSyncedChildren.isNotEmpty,
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.cloud_add),
Text(context.localized.syncAllFiles),
],
),
onTap: () async => _syncRemainingItems(context, syncedItem, unSyncedChildren, ref),
),
PopupMenuItem(
enabled: syncedChildren.isNotEmpty,
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.trash),
Text(context.localized.syncDeleteAll),
],
),
onTap: () async => _deleteSyncedItems(context, syncedItem, syncedChildren, ref),
),
const PopupMenuDivider(),
PopupMenuItem(
enabled: pausedTasks.isNotEmpty,
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.play),
Text(context.localized.syncResumeAll),
],
),
onTap: () => ref
.read(backgroundDownloaderProvider)
.resumeAll(tasks: pausedTasks.map((e) => e.task).nonNulls.toList()),
),
PopupMenuItem(
enabled: runningTasks.isNotEmpty,
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.pause),
Text(context.localized.syncPauseAll),
],
),
onTap: () {
ref
.read(backgroundDownloaderProvider)
.pauseAll(tasks: runningTasks.map((e) => e.task).nonNulls.toList());
},
),
PopupMenuItem(
enabled: [...runningTasks, ...pausedTasks, ...enqueuedTasks].isNotEmpty,
child: Row(
spacing: 12,
children: [
const Icon(IconsaxPlusLinear.stop),
Text(context.localized.syncStopAll),
],
),
onTap: () {
ref.read(backgroundDownloaderProvider).cancelAll(
tasks: [...runningTasks, ...pausedTasks, ...enqueuedTasks].map((e) => e.task).nonNulls.toList());
},
),
]
];
},
);
}
}
Future<dynamic> _deleteSyncedItems(
BuildContext context, SyncedItem syncedItem, List<SyncedItem> syncedChildren, WidgetRef ref) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(context.localized.syncDeleteAllItemsTitle(syncedItem.itemModel?.name ?? "")),
content: Text(
context.localized.syncDeleteAllItemsDesc(syncedItem.itemModel?.name ?? "", syncedChildren.length),
),
scrollable: true,
actions: [
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
FilledButtonAwait(
style: FilledButton.styleFrom(
backgroundColor: Theme.of(context).colorScheme.errorContainer,
foregroundColor: Theme.of(context).colorScheme.onErrorContainer,
iconColor: Theme.of(context).colorScheme.onErrorContainer,
),
onPressed: () async {
final deleteList = syncedChildren.map((e) => ref.read(syncProvider.notifier).deleteFullSyncFiles(e, null));
await Future.wait(deleteList);
Navigator.of(context).pop();
},
child: Text(
context.localized.delete,
),
)
],
),
);
}
Future<dynamic> _syncRemainingItems(
BuildContext context, SyncedItem syncedItem, List<SyncedItem> unSyncedChildren, WidgetRef ref) {
return showDialog(
context: context,
barrierDismissible: false,
builder: (context) => AlertDialog(
title: Text(context.localized.syncAllItemsTitle(syncedItem.itemModel?.name ?? "")),
content: Text(
context.localized.syncAllItemsDesc(
syncedItem.itemModel?.name ?? "",
unSyncedChildren.length,
),
),
scrollable: true,
actions: [
ElevatedButton(onPressed: () => Navigator.of(context).pop(), child: Text(context.localized.cancel)),
FilledButtonAwait(
onPressed: () async {
final syncList = unSyncedChildren.map((e) => ref.read(syncProvider.notifier).syncFile(e, false));
await Future.wait(syncList);
Navigator.of(context).pop();
},
child: Text(
context.localized.sync,
),
)
],
),
);
}

View file

@ -1,17 +1,20 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/syncing/download_stream.dart'; import 'package:fladder/models/syncing/download_stream.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncProgressBuilder extends ConsumerWidget { class SyncProgressBuilder extends ConsumerWidget {
final SyncedItem item; final SyncedItem item;
final List<SyncedItem> children;
final Widget Function(BuildContext context, DownloadStream? combinedStream) builder; final Widget Function(BuildContext context, DownloadStream? combinedStream) builder;
const SyncProgressBuilder({required this.item, required this.builder, super.key}); const SyncProgressBuilder({required this.item, this.children = const [], required this.builder, super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final syncStatus = ref.watch(syncDownloadStatusProvider(item)); final syncStatus = ref.watch(syncDownloadStatusProvider(item, children));
return builder(context, syncStatus); return builder(context, syncStatus);
} }
} }

View file

@ -1,13 +1,16 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package: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/episode_model.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart'; import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
@ -20,12 +23,10 @@ class SyncedEpisodeItem extends ConsumerStatefulWidget {
super.key, super.key,
required this.episode, required this.episode,
required this.syncedItem, required this.syncedItem,
required this.hasFile,
}); });
final EpisodeModel episode; final EpisodeModel episode;
final SyncedItem syncedItem; final SyncedItem syncedItem;
final bool hasFile;
@override @override
ConsumerState<SyncedEpisodeItem> createState() => _SyncedEpisodeItemState(); ConsumerState<SyncedEpisodeItem> createState() => _SyncedEpisodeItemState();
@ -38,87 +39,94 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id)); final downloadTask = ref.watch(downloadTasksProvider(syncedItem.id));
final hasFile = widget.syncedItem.videoFile.existsSync(); final hasFile = widget.syncedItem.videoFile.existsSync();
return Row( return IntrinsicHeight(
children: [ child: Row(
IgnorePointer( children: [
child: ConstrainedBox( ConstrainedBox(
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3), constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3),
child: SizedBox( child: FlatButton(
width: 250, onTap: () {
child: EpisodePoster( widget.episode.navigateTo(context);
episode: widget.episode, return context.maybePop();
syncedItem: syncedItem, },
actions: [], child: SizedBox(
showLabel: false, width: 175,
isCurrentEpisode: false, child: EpisodePoster(
episode: widget.episode,
actions: [],
showLabel: false,
isCurrentEpisode: false,
),
), ),
), ),
), ),
), Expanded(
Expanded( child: Column(
child: Column( mainAxisSize: MainAxisSize.max,
mainAxisSize: MainAxisSize.max, mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Expanded(
Expanded( child: Column(
child: Column( mainAxisAlignment: MainAxisAlignment.center,
mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start, children: [
children: [ Text(
Text( widget.episode.name,
widget.episode.name, style: Theme.of(context).textTheme.titleMedium,
style: Theme.of(context).textTheme.titleMedium,
),
Opacity(
opacity: 0.75,
child: Text(
widget.episode.seasonEpisodeLabel(context),
style: Theme.of(context).textTheme.bodyLarge,
), ),
), Opacity(
], opacity: 0.75,
), child: Text(
), widget.episode.seasonEpisodeLabel(context),
if (!widget.hasFile && downloadTask.hasDownload) style: Theme.of(context).textTheme.bodyLarge,
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,
), ),
) ),
], 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)
if (!hasFile && !downloadTask.hasDownload) IconButtonAwait(
IconButtonAwait( onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false),
onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false), icon: const Icon(IconsaxPlusLinear.cloud_change),
icon: const Icon(IconsaxPlusLinear.cloud_change), )
) else if (hasFile)
else if (hasFile) IconButtonAwait(
IconButtonAwait( color: Theme.of(context).colorScheme.error,
color: Theme.of(context).colorScheme.error, onPressed: () async {
onPressed: () async { await showDefaultAlertDialog(
await showDefaultAlertDialog( context,
context, context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataTitle, context.localized.syncRemoveDataDesc,
context.localized.syncRemoveDataDesc, (context) async {
(context) async { await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task); Navigator.pop(context);
Navigator.pop(context); },
}, context.localized.delete,
context.localized.delete, (context) => Navigator.pop(context),
(context) => Navigator.pop(context), context.localized.cancel,
context.localized.cancel, );
); },
}, icon: const Icon(IconsaxPlusLinear.trash),
icon: const Icon(IconsaxPlusLinear.trash), )
) ].addInBetween(const SizedBox(width: 16)),
].addInBetween(const SizedBox(width: 16)), ),
); );
} }
} }

View file

@ -1,13 +1,21 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/syncing/sync_item.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/animated_fade_size.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/screens/syncing/widgets/synced_episode_item.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart'; import 'package:fladder/util/size_formatting.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncedSeasonPoster extends ConsumerStatefulWidget { class SyncedSeasonPoster extends ConsumerStatefulWidget {
const SyncedSeasonPoster({ const SyncedSeasonPoster({
@ -28,68 +36,96 @@ class _SyncedSeasonPosterState extends ConsumerState<SyncedSeasonPoster> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final season = widget.season; final season = widget.season;
final children = ref.read(syncProvider.notifier).getChildren(widget.syncedItem); final nestedChildren = ref.watch(syncedNestedChildrenProvider(widget.syncedItem));
return Column( return nestedChildren.when(
children: [ data: (children) => Builder(
Row( builder: (context) {
children: [ final syncedItem = widget.syncedItem;
SizedBox( return ExpansionTile(
width: 125, tilePadding: EdgeInsets.zero,
child: AspectRatio( shape: const Border(),
aspectRatio: 0.65, title: Row(
child: Card( spacing: 12,
child: FladderImage( children: [
image: season.getPosters?.primary ?? SizedBox(
season.parentImages?.backDrop?.firstOrNull ?? width: 75,
season.parentImages?.primary, child: AspectRatio(
aspectRatio: 0.65,
child: FlatButton(
onTap: () {
season.navigateTo(context);
return context.maybePop();
},
child: Card(
child: FladderImage(
image: season.getPosters?.primary ??
season.parentImages?.backDrop?.firstOrNull ??
season.parentImages?.primary,
),
),
),
),
),
Flexible(
child: SyncProgressBuilder(
item: syncedItem,
children: children,
builder: (context, combinedStream) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
spacing: 4,
children: [
Flexible(
child: Text(
season.name,
maxLines: 3,
overflow: TextOverflow.ellipsis,
style: Theme.of(context).textTheme.titleMedium,
),
),
Flexible(
child: SyncSubtitle(
syncItem: syncedItem,
children: children,
),
),
Flexible(
child: Consumer(
builder: (context, ref, child) => SyncLabel(
label: context.localized
.totalSize(ref.watch(syncSizeProvider(syncedItem, children))?.byteFormat ?? '--'),
status: combinedStream?.status ?? TaskStatus.notFound,
),
),
),
if (combinedStream != null && combinedStream.hasDownload == true)
SyncProgressBar(item: syncedItem, task: combinedStream)
],
);
},
), ),
), ),
),
),
Column(
children: [
Text(
season.name,
style: Theme.of(context).textTheme.titleMedium,
)
], ],
), ),
const Spacer(), trailing: SyncOptionsButton(syncedItem: syncedItem, children: children),
IconButton( children: children.map(
onPressed: () { (item) {
setState(() { final baseItem = item.itemModel;
expanded = !expanded; return Padding(
}); padding: const EdgeInsets.symmetric(vertical: 8),
child: SyncedEpisodeItem(
episode: baseItem as EpisodeModel,
syncedItem: item,
),
);
}, },
icon: Icon(!expanded ? Icons.keyboard_arrow_down_rounded : Icons.keyboard_arrow_up_rounded), ).toList(),
) );
].addPadding(const EdgeInsets.symmetric(horizontal: 6)), },
), ),
AnimatedFadeSize( error: (error, stackTrace) => const SizedBox.shrink(),
duration: const Duration(milliseconds: 250), loading: () => const SizedBox.shrink(),
child: expanded && children.isNotEmpty
? ListView(
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: <Widget>[
const Divider(),
...children.map(
(item) {
final baseItem = ref.read(syncProvider.notifier).getItem(item);
return IntrinsicHeight(
child: SyncedEpisodeItem(
episode: baseItem as EpisodeModel,
syncedItem: item,
hasFile: item.videoFile.existsSync(),
),
);
},
)
].addPadding(const EdgeInsets.symmetric(vertical: 10)),
)
: Container(),
)
].addPadding(EdgeInsets.only(top: 10, bottom: expanded ? 10 : 0)),
); );
} }
} }

View file

@ -126,9 +126,10 @@ class FladderTheme {
listTileTheme: ListTileThemeData( listTileTheme: ListTileThemeData(
shape: defaultShape, shape: defaultShape,
), ),
dividerTheme: const DividerThemeData( dividerTheme: DividerThemeData(
indent: 6, indent: 6,
endIndent: 6, endIndent: 6,
color: scheme?.onSurface.withAlpha(125),
), ),
segmentedButtonTheme: SegmentedButtonThemeData( segmentedButtonTheme: SegmentedButtonThemeData(
style: ButtonStyle( style: ButtonStyle(

View file

@ -109,8 +109,8 @@ extension ItemBaseModelExtensions on ItemBaseModel {
)) && )) &&
syncAble && syncAble &&
(canDownload ?? false); (canDownload ?? false);
final syncedItem = ref.read(syncProvider.notifier).getSyncedItem(this);
final downloadUrl = ref.read(userProvider.notifier).createDownloadUrl(this); final downloadUrl = ref.read(userProvider.notifier).createDownloadUrl(this);
final syncedItemFuture = ref.read(syncProvider.notifier).getSyncedItem(this);
return [ return [
if (!exclude.contains(ItemActions.play)) if (!exclude.contains(ItemActions.play))
if (playAble) if (playAble)
@ -234,18 +234,39 @@ extension ItemBaseModelExtensions on ItemBaseModel {
), ),
if (!exclude.contains(ItemActions.download) && downloadEnabled) ...[ if (!exclude.contains(ItemActions.download) && downloadEnabled) ...[
if (!kIsWeb) if (!kIsWeb)
if (syncedItem == null) ItemActionButton(
ItemActionButton( icon: FutureBuilder(
icon: const Icon(IconsaxPlusLinear.arrow_down_2), future: syncedItemFuture,
label: Text(context.localized.sync), builder: (context, snapshot) {
action: () => ref.read(syncProvider.notifier).addSyncItem(context, this), final syncedItem = snapshot.data;
) if (syncedItem != null) {
else return IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem));
ItemActionButton( }
icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)), return const Icon(IconsaxPlusLinear.arrow_down_2);
action: () => showSyncItemDetails(context, syncedItem, ref), },
label: Text(context.localized.syncDetails), ),
) 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) ...[ else if (downloadUrl != null) ...[
ItemActionButton( ItemActionButton(
icon: const Icon(IconsaxPlusLinear.document_download), icon: const Icon(IconsaxPlusLinear.document_download),

View file

@ -3,6 +3,7 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/l10n/generated/app_localizations.dart'; import 'package:fladder/l10n/generated/app_localizations.dart';
import 'package:fladder/providers/sync/background_download_provider.dart';
///Only use for base translations, under normal circumstances ALWAYS use the widgets provided context ///Only use for base translations, under normal circumstances ALWAYS use the widgets provided context
final localizationContextProvider = StateProvider<BuildContext?>((ref) => null); final localizationContextProvider = StateProvider<BuildContext?>((ref) => null);
@ -13,7 +14,12 @@ extension BuildContextExtension on BuildContext {
class LocalizationContextWrapper extends ConsumerStatefulWidget { class LocalizationContextWrapper extends ConsumerStatefulWidget {
final Widget child; final Widget child;
const LocalizationContextWrapper({required this.child, super.key}); final Locale currentLocale;
const LocalizationContextWrapper({
required this.child,
required this.currentLocale,
super.key,
});
@override @override
ConsumerState<LocalizationContextWrapper> createState() => _LocalizationContextWrapperState(); ConsumerState<LocalizationContextWrapper> createState() => _LocalizationContextWrapperState();
@ -23,8 +29,21 @@ class _LocalizationContextWrapperState extends ConsumerState<LocalizationContext
@override @override
void initState() { void initState() {
super.initState(); super.initState();
WidgetsBinding.instance.addPostFrameCallback((event) { updateLanguageContext();
}
@override
void didUpdateWidget(covariant LocalizationContextWrapper oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.currentLocale != widget.currentLocale) {
updateLanguageContext();
}
}
void updateLanguageContext() {
WidgetsBinding.instance.addPostFrameCallback((value) {
ref.read(localizationContextProvider.notifier).update((cb) => context); ref.read(localizationContextProvider.notifier).update((cb) => context);
ref.read(backgroundDownloaderProvider.notifier).updateTranslations(context);
}); });
} }

View file

@ -0,0 +1,82 @@
import 'dart:convert';
import 'dart:developer';
import 'dart:io';
import 'package:flutter/foundation.dart';
import 'package:drift/drift.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:isar/isar.dart';
import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:fladder/models/syncing/database_item.dart';
import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/models/syncing/sync_item.dart';
Future<void> isarMigration(Ref ref, AppDatabase db, String savePath) async {
if (kIsWeb) return;
//Return if the database is already migrated or not empty
final isNotEmtpy = await db.select(db.databaseItems).get().then((value) => value.isNotEmpty);
if (isNotEmtpy) {
log('Isar database is not empty, skipping migration');
return;
}
//Open isar database
final applicationDirectory = await getApplicationDocumentsDirectory();
final isarPath = Directory(path.joinAll([applicationDirectory.path, 'Fladder', 'Database']));
await isarPath.create(recursive: true);
final isar = Isar.open(
schemas: [ISyncedItemSchema],
directory: isarPath.path,
);
//Fetch all synced items from the old database
List<SyncedItem> items = isar.iSyncedItems
.where()
.findAll()
.map((e) => SyncedItem.fromIsar(e, path.joinAll([savePath, e.userId ?? "Unkown User"])))
.toList();
//Clear any missing paths
items = items.where((e) => e.path != null ? Directory(e.path!).existsSync() : false).toList();
//Convert to drift database items
final driftItems = items.map(
(item) => DatabaseItemsCompanion(
id: Value(item.id),
parentId: Value(item.parentId),
syncing: Value(item.syncing),
userId: Value(item.userId),
path: Value(item.path),
fileSize: Value(item.fileSize),
sortName: Value(item.sortName),
videoFileName: Value(item.videoFileName),
trickPlayModel: Value(item.fTrickPlayModel != null ? jsonEncode(item.fTrickPlayModel?.toJson()) : null),
mediaSegments: Value(item.mediaSegments != null ? jsonEncode(item.mediaSegments?.toJson()) : null),
images: Value(item.fImages != null ? jsonEncode(item.fImages?.toJson()) : null),
chapters: Value(jsonEncode(item.fChapters.map((e) => e.toJson()).toList())),
subtitles: Value(jsonEncode(item.subtitles.map((e) => e.toJson()).toList())),
userData: Value(item.userData != null ? jsonEncode(item.userData?.toJson()) : null),
),
);
await db.batch((batch) {
batch.insertAll(
db.databaseItems,
driftItems,
mode: InsertMode.insertOrReplace,
);
});
isar.close();
//Delete isar database after a few versions?
// await Future.delayed(const Duration(seconds: 1));
// if (await isarPath.exists()) {
// log('Deleting old Fladder base folder: ${isarPath.path}');
// await isarPath.delete(recursive: true);
// }
}

View file

@ -35,6 +35,8 @@ extension RefreshContextExtension on BuildContext {
Future<void> refreshData() async { Future<void> refreshData() async {
//Small delay to fix server not updating response based on successful query //Small delay to fix server not updating response based on successful query
await Future.delayed(const Duration(milliseconds: 250)); await Future.delayed(const Duration(milliseconds: 250));
await RefreshState.maybeOf(this)?.refresh(); if (mounted) {
await RefreshState.maybeOf(this)?.refresh();
}
} }
} }

View file

@ -59,8 +59,9 @@ class _BackgroundImageState extends ConsumerState<BackgroundImage> {
if (itemId == null) return; if (itemId == null) return;
final apiResponse = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet(itemId: itemId); final apiResponse = await ref.read(jellyApiProvider).usersUserIdItemsItemIdGet(itemId: itemId);
final image = final image = apiResponse.body?.parentBaseModel.getPosters?.randomBackDrop ??
apiResponse.body?.parentBaseModel.getPosters?.randomBackDrop ?? apiResponse.body?.getPosters?.randomBackDrop; apiResponse.body?.getPosters?.randomBackDrop ??
apiResponse.body?.getPosters?.primary;
if (mounted) setState(() => backgroundImage = image); if (mounted) setState(() => backgroundImage = image);
}); });

View file

@ -30,7 +30,7 @@ class ActionContent extends StatelessWidget {
height: 4, height: 4,
), ),
], ],
Expanded(child: child), Flexible(child: child),
if (actions.isNotEmpty) ...[ if (actions.isNotEmpty) ...[
if (showDividers) if (showDividers)
const Divider( const Divider(

View file

@ -1,9 +1,10 @@
import 'dart:async'; import 'dart:async';
import 'dart:developer'; import 'dart:developer';
import 'package:fladder/screens/shared/animated_fade_size.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart';
class IconButtonAwait extends StatefulWidget { class IconButtonAwait extends StatefulWidget {
final FutureOr<dynamic> Function() onPressed; final FutureOr<dynamic> Function() onPressed;
final Color? color; final Color? color;
@ -33,7 +34,11 @@ class IconButtonAwaitState extends State<IconButtonAwait> {
} catch (e) { } catch (e) {
log(e.toString()); log(e.toString());
} finally { } finally {
setState(() => loading = false); if (mounted) {
setState(() {
loading = false;
});
}
} }
}, },
icon: AnimatedFadeSize( icon: AnimatedFadeSize(

View file

@ -12,7 +12,7 @@ class StatusCard extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
return Padding( return Padding(
padding: const EdgeInsets.all(5), padding: const EdgeInsets.all(2),
child: SizedBox.square( child: SizedBox.square(
dimension: 33, dimension: 33,
child: Card( child: Card(

View file

@ -13,6 +13,7 @@
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h> #include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_plugin.h> #include <media_kit_video/media_kit_video_plugin.h>
#include <screen_retriever_linux/screen_retriever_linux_plugin.h> #include <screen_retriever_linux/screen_retriever_linux_plugin.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_linux/url_launcher_plugin.h> #include <url_launcher_linux/url_launcher_plugin.h>
#include <volume_controller/volume_controller_plugin.h> #include <volume_controller/volume_controller_plugin.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
@ -39,6 +40,9 @@ void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar = g_autoptr(FlPluginRegistrar) screen_retriever_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar); screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar = g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin"); fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar); url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View file

@ -10,6 +10,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
media_kit_libs_linux media_kit_libs_linux
media_kit_video media_kit_video
screen_retriever_linux screen_retriever_linux
sqlite3_flutter_libs
url_launcher_linux url_launcher_linux
volume_controller volume_controller
window_manager window_manager

View file

@ -24,6 +24,7 @@ import screen_retriever_macos
import share_plus import share_plus
import shared_preferences_foundation import shared_preferences_foundation
import sqflite_darwin import sqflite_darwin
import sqlite3_flutter_libs
import url_launcher_macos import url_launcher_macos
import video_player_avfoundation import video_player_avfoundation
import volume_controller import volume_controller
@ -51,6 +52,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin")) SharePlusMacosPlugin.register(with: registry.registrar(forPlugin: "SharePlusMacosPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin")) SqflitePlugin.register(with: registry.registrar(forPlugin: "SqflitePlugin"))
Sqlite3FlutterLibsPlugin.register(with: registry.registrar(forPlugin: "Sqlite3FlutterLibsPlugin"))
UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin"))
FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin"))
VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin")) VolumeControllerPlugin.register(with: registry.registrar(forPlugin: "VolumeControllerPlugin"))

View file

@ -146,10 +146,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build name: build
sha256: cef23f1eda9b57566c81e2133d196f8e3df48f244b317368d65c5943d91148f0 sha256: "51dc711996cbf609b90cbe5b335bbce83143875a9d58e4b5c6d3c4f684d3dda7"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.2" version: "2.5.4"
build_cli_annotations: build_cli_annotations:
dependency: transitive dependency: transitive
description: description:
@ -178,26 +178,26 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: build_resolvers name: build_resolvers
sha256: b9e4fda21d846e192628e7a4f6deda6888c36b5b69ba02ff291a01fd529140f0 sha256: ee4257b3f20c0c90e72ed2b57ad637f694ccba48839a821e87db762548c22a62
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.4" version: "2.5.4"
build_runner: build_runner:
dependency: "direct dev" dependency: "direct dev"
description: description:
name: build_runner name: build_runner
sha256: "058fe9dce1de7d69c4b84fada934df3e0153dd000758c4d65964d0166779aa99" sha256: "382a4d649addbfb7ba71a3631df0ec6a45d5ab9b098638144faf27f02778eb53"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.15" version: "2.5.4"
build_runner_core: build_runner_core:
dependency: transitive dependency: transitive
description: description:
name: build_runner_core name: build_runner_core
sha256: "22e3aa1c80e0ada3722fe5b63fd43d9c8990759d0a2cf489c8c5d7b2bdebc021" sha256: "85fbbb1036d576d966332a3f5ce83f2ce66a40bea1a94ad2d5fc29a19a0d3792"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "8.0.0" version: "9.1.2"
built_collection: built_collection:
dependency: transitive dependency: transitive
description: description:
@ -430,6 +430,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.8" version: "2.3.8"
db_viewer:
dependency: transitive
description:
name: db_viewer
sha256: "5f7e3cfcde9663321797d8f6f0c876f7c13f0825a2e77ec1ef065656797144d9"
url: "https://pub.dev"
source: hosted
version: "1.1.0"
dbus: dbus:
dependency: transitive dependency: transitive
description: description:
@ -438,6 +446,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "0.7.11" version: "0.7.11"
decimal:
dependency: transitive
description:
name: decimal
sha256: fc706a5618b81e5b367b01dd62621def37abc096f2b46a9bd9068b64c1fa36d0
url: "https://pub.dev"
source: hosted
version: "3.2.4"
desktop_drop: desktop_drop:
dependency: "direct main" dependency: "direct main"
description: description:
@ -454,6 +470,46 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "3.0.0" version: "3.0.0"
drift:
dependency: "direct main"
description:
name: drift
sha256: b584ddeb2b74436735dd2cf746d2d021e19a9a6770f409212fd5cbc2814ada85
url: "https://pub.dev"
source: hosted
version: "2.26.1"
drift_db_viewer:
dependency: "direct main"
description:
name: drift_db_viewer
sha256: "5ea77858c52b55460a1e8f34ab5f88324621d486717d876fd745765fbc227f3f"
url: "https://pub.dev"
source: hosted
version: "2.1.0"
drift_dev:
dependency: "direct dev"
description:
name: drift_dev
sha256: "0d3f8b33b76cf1c6a82ee34d9511c40957549c4674b8f1688609e6d6c7306588"
url: "https://pub.dev"
source: hosted
version: "2.26.0"
drift_flutter:
dependency: "direct main"
description:
name: drift_flutter
sha256: ccfb42bc942e59f81500b16228df59cf8eb40d2fbd96637ff677df923350af7b
url: "https://pub.dev"
source: hosted
version: "0.2.5"
drift_sync:
dependency: "direct main"
description:
name: drift_sync
sha256: "522f8e651ceb9dcfb44b576593ac2d2eae79f0663d7418fb7804b3cc4d9d27e3"
url: "https://pub.dev"
source: hosted
version: "0.13.0"
dynamic_color: dynamic_color:
dependency: "direct main" dependency: "direct main"
description: description:
@ -514,10 +570,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.9" version: "10.2.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -842,6 +898,22 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.1.3" version: "2.1.3"
google_identity_services_web:
dependency: transitive
description:
name: google_identity_services_web
sha256: "5d187c46dc59e02646e10fe82665fc3884a9b71bc1c90c2b8b749316d33ee454"
url: "https://pub.dev"
source: hosted
version: "0.3.3+1"
googleapis_auth:
dependency: transitive
description:
name: googleapis_auth
sha256: b81fe352cc4a330b3710d2b7ad258d9bcef6f909bb759b306bf42973a7d046db
url: "https://pub.dev"
source: hosted
version: "2.0.0"
graphs: graphs:
dependency: transitive dependency: transitive
description: description:
@ -850,6 +922,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.3.2" version: "2.3.2"
grpc:
dependency: transitive
description:
name: grpc
sha256: "2dde469ddd8bbd7a33a0765da417abe1ad2142813efce3a86c512041294e2b26"
url: "https://pub.dev"
source: hosted
version: "4.1.0"
highlight: highlight:
dependency: transitive dependency: transitive
description: description:
@ -874,6 +954,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.4.0" version: "1.4.0"
http2:
dependency: transitive
description:
name: http2
sha256: "382d3aefc5bd6dc68c6b892d7664f29b5beb3251611ae946a98d35158a82bbfa"
url: "https://pub.dev"
source: hosted
version: "2.3.1"
http_client_helper: http_client_helper:
dependency: transitive dependency: transitive
description: description:
@ -942,18 +1030,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: isar name: isar
sha256: ebf74d87c400bd9f7da14acb31932b50c2407edbbd40930da3a6c2a8143f85a8 sha256: e987032e5d007a03ba4415cbf4d47add17b57ac664db8705db90fbfeb6a16737
url: "https://pub.dev" url: "https://pub.isar-community.dev"
source: hosted source: hosted
version: "4.0.0-dev.14" version: "4.0.3"
isar_flutter_libs: isar_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
name: isar_flutter_libs name: isar_flutter_libs
sha256: "04a3f4035e213ddb6e78d0132a7c80296a085c2088c2a761b4a42ee5add36983" sha256: a6b86d8618fe2d7d0e2ac6aa7a7f21c0c8ae912ccbef94a45d9f6e1e519ef610
url: "https://pub.dev" url: "https://pub.isar-community.dev"
source: hosted source: hosted
version: "4.0.0-dev.14" version: "4.0.3"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -1402,6 +1490,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "6.0.2" version: "6.0.2"
protobuf:
dependency: transitive
description:
name: protobuf
sha256: "6153efcc92a06910918f3db8231fd2cf828ac81e50ebd87adc8f8a8cb3caff0e"
url: "https://pub.dev"
source: hosted
version: "4.1.1"
provider: provider:
dependency: transitive dependency: transitive
description: description:
@ -1434,6 +1530,14 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.3.7+1" version: "1.3.7+1"
rational:
dependency: transitive
description:
name: rational
sha256: cb808fb6f1a839e6fc5f7d8cb3b0a10e1db48b3be102de73938c627f0b636336
url: "https://pub.dev"
source: hosted
version: "2.2.3"
recase: recase:
dependency: transitive dependency: transitive
description: description:
@ -1791,6 +1895,30 @@ packages:
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "2.4.0" version: "2.4.0"
sqlite3:
dependency: transitive
description:
name: sqlite3
sha256: "608b56d594e4c8498c972c8f1507209f9fd74939971b948ddbbfbfd1c9cb3c15"
url: "https://pub.dev"
source: hosted
version: "2.7.7"
sqlite3_flutter_libs:
dependency: transitive
description:
name: sqlite3_flutter_libs
sha256: "8b4bd239bedd20ee628aed587b4c5b387328e85945c9ecbae19a93bdcd171524"
url: "https://pub.dev"
source: hosted
version: "0.5.36"
sqlparser:
dependency: transitive
description:
name: sqlparser
sha256: "7c859c803cf7e9a84d6db918bac824545045692bbe94a6386bd3a45132235d09"
url: "https://pub.dev"
source: hosted
version: "0.41.1"
square_progress_indicator: square_progress_indicator:
dependency: "direct main" dependency: "direct main"
description: description:
@ -2027,10 +2155,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics name: vector_graphics
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.18" version: "1.1.19"
vector_graphics_codec: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:
@ -2139,10 +2267,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: watcher name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
weak_map: weak_map:
dependency: transitive dependency: transitive
description: description:
@ -2211,10 +2339,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.14.0"
window_manager: window_manager:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -98,7 +98,7 @@ dependencies:
# Utility # Utility
path: ^1.9.1 path: ^1.9.1
file_picker: ^10.1.9 file_picker: ^10.2.0
transparent_image: ^2.0.1 transparent_image: ^2.0.1
universal_html: ^2.2.4 universal_html: ^2.2.4
collection: ^1.19.1 collection: ^1.19.1
@ -114,8 +114,12 @@ dependencies:
screen_retriever: ^0.2.0 screen_retriever: ^0.2.0
# Data # Data
isar: ^4.0.0-dev.14 isar:
isar_flutter_libs: ^4.0.0-dev.14 # contains Isar Core version: ^4.0.3
hosted: https://pub.isar-community.dev/
isar_flutter_libs: # contains Isar Core
version: ^4.0.3
hosted: https://pub.isar-community.dev/
# Other # Other
async: ^2.13.0 async: ^2.13.0
@ -126,6 +130,10 @@ dependencies:
share_plus: ^11.0.0 share_plus: ^11.0.0
archive: ^4.0.7 archive: ^4.0.7
dart_mappable: ^4.5.0 dart_mappable: ^4.5.0
drift: ^2.26.1
drift_flutter: ^0.2.5
drift_sync: ^0.13.0
drift_db_viewer: ^2.1.0
dev_dependencies: dev_dependencies:
flutter_test: flutter_test:
@ -136,8 +144,9 @@ dev_dependencies:
# activated in the `analysis_options.yaml` file located at the root of your # activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint # package. See that file for information about deactivating specific lint
# rules and activating additional ones. # rules and activating additional ones.
drift_dev: ^2.26.0
flutter_lints: ^6.0.0 flutter_lints: ^6.0.0
build_runner: ^2.4.15 build_runner: ^2.5.4
chopper_generator: ^8.1.0 chopper_generator: ^8.1.0
json_serializable: ^6.9.0 json_serializable: ^6.9.0
custom_lint: ^0.7.0 custom_lint: ^0.7.0

View file

@ -17,6 +17,7 @@
#include <screen_brightness_windows/screen_brightness_windows_plugin.h> #include <screen_brightness_windows/screen_brightness_windows_plugin.h>
#include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h> #include <screen_retriever_windows/screen_retriever_windows_plugin_c_api.h>
#include <share_plus/share_plus_windows_plugin_c_api.h> #include <share_plus/share_plus_windows_plugin_c_api.h>
#include <sqlite3_flutter_libs/sqlite3_flutter_libs_plugin.h>
#include <url_launcher_windows/url_launcher_windows.h> #include <url_launcher_windows/url_launcher_windows.h>
#include <volume_controller/volume_controller_plugin_c_api.h> #include <volume_controller/volume_controller_plugin_c_api.h>
#include <window_manager/window_manager_plugin.h> #include <window_manager/window_manager_plugin.h>
@ -44,6 +45,8 @@ void RegisterPlugins(flutter::PluginRegistry* registry) {
registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi")); registry->GetRegistrarForPlugin("ScreenRetrieverWindowsPluginCApi"));
SharePlusWindowsPluginCApiRegisterWithRegistrar( SharePlusWindowsPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi")); registry->GetRegistrarForPlugin("SharePlusWindowsPluginCApi"));
Sqlite3FlutterLibsPluginRegisterWithRegistrar(
registry->GetRegistrarForPlugin("Sqlite3FlutterLibsPlugin"));
UrlLauncherWindowsRegisterWithRegistrar( UrlLauncherWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("UrlLauncherWindows")); registry->GetRegistrarForPlugin("UrlLauncherWindows"));
VolumeControllerPluginCApiRegisterWithRegistrar( VolumeControllerPluginCApiRegisterWithRegistrar(

View file

@ -14,6 +14,7 @@ list(APPEND FLUTTER_PLUGIN_LIST
screen_brightness_windows screen_brightness_windows
screen_retriever_windows screen_retriever_windows
share_plus share_plus
sqlite3_flutter_libs
url_launcher_windows url_launcher_windows
volume_controller volume_controller
window_manager window_manager