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": [
"--web-port",
"9090",
"--web-experimental-hot-reload"
],
},
{
@ -93,7 +92,6 @@
"args": [
"--web-port",
"9090",
"--web-experimental-hot-reload"
],
},
],

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -50,6 +50,21 @@ class UserData with UserDataMappable {
Duration get playBackPosition => Duration(milliseconds: playbackPositionTicks ~/ 10000);
static UserData? determineLastUserData(List<UserData?> data) {
return data.where((data) => data != null).reduce((a, b) {
final aDate = a?.lastPlayed;
final bDate = b?.lastPlayed;
if (aDate != null && bDate != null) {
return aDate.isAfter(bDate) ? a : b;
} else if (aDate != null) {
return a;
} else {
return b;
}
});
}
factory UserData.fromMap(Map<String, dynamic> map) => UserDataMapper.fromMap(map);
factory UserData.fromJson(String json) => UserDataMapper.fromJson(json);
}

View file

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

View file

@ -2,6 +2,7 @@ import 'dart:developer';
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:chopper/chopper.dart';
import 'package:collection/collection.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
@ -26,7 +27,6 @@ import 'package:fladder/profiles/default_profile.dart';
import 'package:fladder/providers/api_provider.dart';
import 'package:fladder/providers/service_provider.dart';
import 'package:fladder/providers/settings/video_player_settings_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/providers/video_player_provider.dart';
@ -129,7 +129,7 @@ class PlaybackModelHelper {
await _createOfflinePlaybackModel(
newItem,
null,
ref.read(syncProvider.notifier).getSyncedItem(newItem),
await ref.read(syncProvider.notifier).getSyncedItem(newItem),
oldModel: currentModel,
);
if (newModel == null) return null;
@ -143,10 +143,10 @@ class PlaybackModelHelper {
SyncedItem? syncedItem, {
PlaybackModel? oldModel,
}) async {
final ItemBaseModel? syncedItemModel = ref.read(syncProvider.notifier).getItem(syncedItem);
final ItemBaseModel? syncedItemModel = syncedItem?.itemModel;
if (syncedItemModel == null || syncedItem == null || !syncedItem.dataFile.existsSync()) return null;
final children = ref.read(syncChildrenProvider(syncedItem));
final children = await ref.read(syncProvider.notifier).getChildren(syncedItem);
final syncedItems = children.where((element) => element.videoFile.existsSync()).toList();
final itemQueue = syncedItems.map((e) => e.createItemModel(ref));
@ -174,67 +174,81 @@ class PlaybackModelHelper {
final userId = ref.read(userProvider)?.id;
if (userId?.isEmpty == true) return null;
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
try {
final queue = oldModel?.queue ?? libraryQueue ?? await collectQueue(item);
final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (queue.whereType<EpisodeModel>().toList().nextUp),
_ => item,
};
if (firstItemToPlay == null) return null;
final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body;
if (fullItem == null) return null;
SyncedItem? syncedItem = ref.read(syncProvider.notifier).getSyncedItem(fullItem);
final firstItemIsSynced = syncedItem != null && syncedItem.status == SyncStatus.complete;
final options = {
PlaybackType.directStream,
PlaybackType.transcode,
if (firstItemIsSynced) PlaybackType.offline,
};
if ((showPlaybackOptions || firstItemIsSynced) && context != null) {
final playbackType = await showPlaybackTypeSelection(
context: context,
options: options,
);
if (!context.mounted) return null;
return switch (playbackType) {
PlaybackType.directStream || PlaybackType.transcode => await _createServerPlaybackModel(
fullItem,
item.streamModel,
playbackType,
oldModel: oldModel,
libraryQueue: queue,
startPosition: startPosition,
),
PlaybackType.offline => await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
),
null => null
final firstItemToPlay = switch (item) {
SeriesModel _ || SeasonModel _ => (queue.whereType<EpisodeModel>().toList().nextUp),
_ => item,
};
} else {
return (await _createServerPlaybackModel(
fullItem,
item.streamModel,
PlaybackType.directStream,
startPosition: startPosition,
oldModel: oldModel,
libraryQueue: queue,
)) ??
await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
);
if (firstItemToPlay == null) return null;
final fullItem = (await api.usersUserIdItemsItemIdGet(itemId: firstItemToPlay.id)).body;
if (fullItem == null) return null;
SyncedItem? syncedItem = await ref.read(syncProvider.notifier).getSyncedItem(fullItem);
final firstItemIsSynced = syncedItem != null && syncedItem.status == TaskStatus.complete;
final options = {
PlaybackType.directStream,
PlaybackType.transcode,
if (firstItemIsSynced) PlaybackType.offline,
};
if ((showPlaybackOptions || firstItemIsSynced) && context != null) {
final playbackType = await showPlaybackTypeSelection(
context: context,
options: options,
);
if (!context.mounted) return null;
return switch (playbackType) {
PlaybackType.directStream || PlaybackType.transcode => await _createServerPlaybackModel(
fullItem,
item.streamModel,
playbackType,
oldModel: oldModel,
libraryQueue: queue,
startPosition: startPosition,
),
PlaybackType.offline => await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
),
null => null
};
} else {
return (await _createServerPlaybackModel(
fullItem,
item.streamModel,
PlaybackType.directStream,
startPosition: startPosition,
oldModel: oldModel,
libraryQueue: queue,
)) ??
await _createOfflinePlaybackModel(
fullItem,
item.streamModel,
syncedItem,
);
}
} catch (e) {
log(e.toString());
SyncedItem? syncedItem = await ref.read(syncProvider.notifier).getSyncedItem(item);
if (syncedItem != null) {
return await _createOfflinePlaybackModel(
item,
item.streamModel,
syncedItem,
oldModel: oldModel,
);
}
return null;
}
}

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';
// extension IsarExtensions on String? {
// int get fastHash {
// if (this == null) return 0;
// var hash = 0xcbf29ce484222325;
// var i = 0;
// while (i < this!.length) {
// final codeUnit = this!.codeUnitAt(i++);
// hash ^= codeUnit >> 8;
// hash *= 0x100000001b3;
// hash ^= codeUnit & 0xFF;
// hash *= 0x100000001b3;
// }
// return hash;
// }
// }
@collection
class ISyncedItem {
String? userId;
String id;
bool syncing;
String? sortName;
@Index()
String? parentId;
String? path;
int? fileSize;

View file

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

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

View file

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

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

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 syncedItem = syncNotifier.getParentItem(item.id);
final syncedItem = await syncNotifier.getParentItem(item.id);
if (syncedItem == null) return;
final movieModel = syncedItem.createItemModel(ref) as MovieModel;
final movieModel = syncedItem.itemModel as MovieModel?;
state = movieModel;
}

View file

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

View file

@ -29,7 +29,12 @@ class SeasonDetailsNotifier extends StateNotifier<SeasonModel?> {
seriesId: newState?.seriesId ?? "",
seasonId: newState?.id,
season: newState?.season,
fields: [ItemFields.overview],
fields: [
ItemFields.overview,
ItemFields.candelete,
ItemFields.candownload,
ItemFields.parentid,
],
);
newState = newState?.copyWith(episodes: EpisodeModel.episodesFromDto(episodes.body?.items, ref).toList());
state = newState;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/book_model.dart';
import 'package:fladder/models/items/images_models.dart';

View file

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

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/items/images_models.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/routes/auto_router.gr.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:fladder/theme.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/fladder_image.dart';
@ -196,6 +199,19 @@ class _DetailScaffoldState extends ConsumerState<DetailScaffold> {
mainAxisSize: MainAxisSize.min,
children: [
if (widget.item != null) ...[
ref.watch(syncedItemProvider(widget.item)).when(
error: (error, stackTrace) => const SizedBox.shrink(),
data: (syncedItem) {
if (syncedItem == null) {
return const SizedBox.shrink();
}
return IconButton(
onPressed: () => showSyncItemDetails(context, syncedItem, ref),
icon: SyncButton(item: widget.item!, syncedItem: syncedItem),
);
},
loading: () => const SizedBox.shrink(),
),
Builder(
builder: (context) {
final newActions = widget.actions?.call(context);

View file

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

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

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

View file

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

View file

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

View file

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

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

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

View file

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

View file

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

View file

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

View file

@ -1,3 +1,4 @@
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
@ -52,6 +53,30 @@ class _SyncedScreenState extends ConsumerState<SyncedScreen> {
)
else
const DefaultSliverTopBadding(),
if (kDebugMode)
SliverToBoxAdapter(
child: Padding(
padding: padding,
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
spacing: 12,
children: [
ElevatedButton(
onPressed: () => ref.read(syncProvider.notifier).viewDatabase(context),
child: const Text("View Database"),
),
ElevatedButton(
onPressed: () => ref.read(syncProvider.notifier).db.clearDatabase(),
child: const Text("Clear drift database"),
),
ElevatedButton(
onPressed: () => ref.read(syncProvider.notifier).migrateFromIsar(),
child: const Text("Migrate Isar to Drift"),
),
],
),
),
),
if (items.isNotEmpty) ...[
SliverToBoxAdapter(
child: Padding(

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

View file

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

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

View file

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

View file

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

View file

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

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 {
//Small delay to fix server not updating response based on successful query
await Future.delayed(const Duration(milliseconds: 250));
await RefreshState.maybeOf(this)?.refresh();
if (mounted) {
await RefreshState.maybeOf(this)?.refresh();
}
}
}

View file

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

View file

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

View file

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

View file

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

View file

@ -13,6 +13,7 @@
#include <media_kit_libs_linux/media_kit_libs_linux_plugin.h>
#include <media_kit_video/media_kit_video_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 <volume_controller/volume_controller_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 =
fl_plugin_registry_get_registrar_for_plugin(registry, "ScreenRetrieverLinuxPlugin");
screen_retriever_linux_plugin_register_with_registrar(screen_retriever_linux_registrar);
g_autoptr(FlPluginRegistrar) sqlite3_flutter_libs_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "Sqlite3FlutterLibsPlugin");
sqlite3_flutter_libs_plugin_register_with_registrar(sqlite3_flutter_libs_registrar);
g_autoptr(FlPluginRegistrar) url_launcher_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "UrlLauncherPlugin");
url_launcher_plugin_register_with_registrar(url_launcher_linux_registrar);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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