feature: Improved sync capability

This commit is contained in:
PartyDonut 2025-06-09 21:23:02 +02:00
parent f3e920ac79
commit c5c7f71b84
31 changed files with 500 additions and 344 deletions

View file

@ -1238,5 +1238,9 @@
}, },
"newUpdateFoundOnGithub": "Found a new update on Github", "newUpdateFoundOnGithub": "Found a new update on Github",
"enableBackgroundPostersTitle": "Enable background posters", "enableBackgroundPostersTitle": "Enable background posters",
"enableBackgroundPostersDesc": "Show random posters in applicable screens" "enableBackgroundPostersDesc": "Show random posters in applicable screens",
"notificationDownloadingDownloading": "Downloading",
"notificationDownloadingPaused": "Download paused",
"notificationDownloadingFinished": "Download finished",
"notificationDownloadingError": "Download error"
} }

View file

@ -297,6 +297,7 @@ class _MainState extends ConsumerState<Main> with WindowListener, WidgetsBinding
}, },
builder: (context, child) => LocalizationContextWrapper( builder: (context, child) => LocalizationContextWrapper(
child: ScaffoldMessenger(child: child ?? Container()), child: ScaffoldMessenger(child: child ?? Container()),
currentLocale: language,
), ),
debugShowCheckedModeBanner: false, debugShowCheckedModeBanner: false,
darkTheme: darkTheme.copyWith( darkTheme: darkTheme.copyWith(

View file

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

View file

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

View file

@ -30,6 +30,7 @@ class ISyncedItem {
String id; String id;
bool syncing; bool syncing;
String? sortName; String? sortName;
@Index()
String? parentId; String? parentId;
String? path; String? path;
int? fileSize; int? fileSize;

View file

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

View file

@ -47,6 +47,7 @@ class SyncedItem with _$SyncedItem {
}) = _SyncItem; }) = _SyncItem;
static String trickPlayPath = "TrickPlay"; static String trickPlayPath = "TrickPlay";
static String chaptersPath = "Chapters";
List<Chapter> get chapters => fChapters.map((e) => e.copyWith(imageUrl: joinAll({"$path", e.imageUrl}))).toList(); List<Chapter> get chapters => fChapters.map((e) => e.copyWith(imageUrl: joinAll({"$path", e.imageUrl}))).toList();
@ -94,6 +95,7 @@ class SyncedItem with _$SyncedItem {
try { try {
await videoFile.delete(); await videoFile.delete();
await Directory(joinAll([directory.path, trickPlayPath])).delete(recursive: true); await Directory(joinAll([directory.path, trickPlayPath])).delete(recursive: true);
await Directory(joinAll([directory.path, chaptersPath])).delete(recursive: true);
} catch (e) { } catch (e) {
return false; return false;
} }

View file

@ -32,20 +32,29 @@ class SeriesDetailViewNotifier extends StateNotifier<SeriesModel?> {
final response = await api.usersUserIdItemsItemIdGet(itemId: seriesModel.id); final response = await api.usersUserIdItemsItemIdGet(itemId: seriesModel.id);
if (response.body == null) return null; if (response.body == null) return null;
newState = response.bodyOrThrow as SeriesModel; newState = response.bodyOrThrow as SeriesModel;
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id);
newState = newState.copyWith(seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref));
final episodes = await api.showsSeriesIdEpisodesGet(seriesId: seriesModel.id, fields: [ final episodes = await api.showsSeriesIdEpisodesGet(seriesId: seriesModel.id, fields: [
ItemFields.mediastreams, ItemFields.mediastreams,
ItemFields.mediasources, ItemFields.mediasources,
ItemFields.overview, ItemFields.overview,
ItemFields.candownload,
]); ]);
final newEpisodes = EpisodeModel.episodesFromDto(
episodes.body?.items,
ref,
);
final episodesCanDownload = newEpisodes.any((episode) => episode.canDownload == true);
final seasons = await api.showsSeriesIdSeasonsGet(seriesId: seriesModel.id);
newState = newState.copyWith( newState = newState.copyWith(
availableEpisodes: EpisodeModel.episodesFromDto( seasons: SeasonModel.seasonsFromDto(seasons.body?.items, ref)
episodes.body?.items, .map((element) => element.copyWith(canDownload: true))
ref, .toList(),
), );
newState = newState.copyWith(
canDownload: episodesCanDownload,
availableEpisodes: newEpisodes,
); );
final related = await ref.read(relatedUtilityProvider).relatedContent(seriesModel.id); final related = await ref.read(relatedUtilityProvider).relatedContent(seriesModel.id);

View file

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

View file

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

View file

@ -11,30 +11,40 @@ part 'sync_provider_helpers.g.dart';
@riverpod @riverpod
class SyncChildren extends _$SyncChildren { class SyncChildren extends _$SyncChildren {
@override @override
List<SyncedItem> build(SyncedItem arg) { List<SyncedItem> build(SyncedItem root) {
final syncedItemIsar = ref.watch(syncProvider.notifier).isar; final isar = ref.watch(syncProvider.notifier).isar;
final allChildren = <SyncedItem>[]; final syncPath = ref.read(syncProvider.notifier).syncPath ?? "";
List<SyncedItem> toProcess = [arg];
if (isar == null) return [];
final all = <SyncedItem>[];
List<SyncedItem> toProcess = [root];
while (toProcess.isNotEmpty) { while (toProcess.isNotEmpty) {
final currentLevel = toProcess.map( final parentIds = toProcess.map((e) => e.id).toList();
(parent) {
final children = syncedItemIsar?.iSyncedItems.where().parentIdEqualTo(parent.id).sortBySortName().findAll(); final children = <ISyncedItem>[];
return children?.map((e) => SyncedItem.fromIsar(e, ref.read(syncProvider.notifier).syncPath ?? "")) ?? for (final id in parentIds) {
<SyncedItem>[]; final results = isar.iSyncedItems.where().parentIdEqualTo(id).sortBySortName().findAll();
}, children.addAll(results);
); }
allChildren.addAll(currentLevel.expand((list) => list));
toProcess = currentLevel.expand((list) => list).toList(); if (children.isEmpty) break;
final wrapped = children.map((e) => SyncedItem.fromIsar(e, syncPath)).toList();
all.addAll(wrapped);
toProcess = wrapped;
} }
return allChildren;
return all;
} }
} }
@riverpod @riverpod
class SyncDownloadStatus extends _$SyncDownloadStatus { class SyncDownloadStatus extends _$SyncDownloadStatus {
@override @override
DownloadStream? build(SyncedItem arg) { DownloadStream? build(SyncedItem arg, List<SyncedItem> children) {
final nestedChildren = ref.watch(syncChildrenProvider(arg)); final nestedChildren = children;
ref.watch(downloadTasksProvider(arg.id)); ref.watch(downloadTasksProvider(arg.id));
for (var element in nestedChildren) { for (var element in nestedChildren) {
@ -64,20 +74,23 @@ class SyncDownloadStatus extends _$SyncDownloadStatus {
@riverpod @riverpod
class SyncStatuses extends _$SyncStatuses { class SyncStatuses extends _$SyncStatuses {
@override @override
FutureOr<SyncStatus> build(SyncedItem arg) async { FutureOr<SyncStatus> build(SyncedItem arg, List<SyncedItem>? children) async {
final nestedChildren = ref.watch(syncChildrenProvider(arg)); final nestedChildren = children;
ref.watch(downloadTasksProvider(arg.id)); ref.watch(downloadTasksProvider(arg.id));
for (var element in nestedChildren) { if (nestedChildren != null) {
ref.watch(downloadTasksProvider(element.id)); for (var element in nestedChildren) {
} ref.watch(downloadTasksProvider(element.id));
}
for (var i = 0; i < nestedChildren.length; i++) { for (var i = 0; i < nestedChildren.length; i++) {
final item = nestedChildren[i]; final item = nestedChildren[i];
if (item.hasVideoFile && !await item.videoFile.exists()) { if (item.hasVideoFile && !await item.videoFile.exists()) {
return SyncStatus.partially; return SyncStatus.partially;
}
} }
} }
if (arg.hasVideoFile && !await arg.videoFile.exists()) { if (arg.hasVideoFile && !await arg.videoFile.exists()) {
return SyncStatus.partially; return SyncStatus.partially;
} }
@ -88,17 +101,21 @@ class SyncStatuses extends _$SyncStatuses {
@riverpod @riverpod
class SyncSize extends _$SyncSize { class SyncSize extends _$SyncSize {
@override @override
int? build(SyncedItem arg) { int? build(SyncedItem arg, List<SyncedItem>? children) {
final nestedChildren = ref.watch(syncChildrenProvider(arg)); final nestedChildren = children;
ref.watch(downloadTasksProvider(arg.id)); ref.watch(downloadTasksProvider(arg.id));
for (var element in nestedChildren) {
ref.watch(downloadTasksProvider(element.id));
}
int size = arg.fileSize ?? 0; int size = arg.fileSize ?? 0;
for (var element in nestedChildren) {
size += element.fileSize ?? 0; if (nestedChildren != null) {
for (var element in nestedChildren) {
ref.watch(downloadTasksProvider(element.id));
}
for (var element in nestedChildren) {
size += element.fileSize ?? 0;
}
} }
return size; return size;
} }
} }

View file

@ -6,7 +6,7 @@ part of 'sync_provider_helpers.dart';
// RiverpodGenerator // RiverpodGenerator
// ************************************************************************** // **************************************************************************
String _$syncChildrenHash() => r'f6fdb1aa36d6655976baa5fbe0d8a6b812d7e95b'; String _$syncChildrenHash() => r'c5a90d630d49f59ad4fbaacb5154f1205799f5ab';
/// Copied from Dart SDK /// Copied from Dart SDK
class _SystemHash { class _SystemHash {
@ -31,10 +31,10 @@ class _SystemHash {
abstract class _$SyncChildren abstract class _$SyncChildren
extends BuildlessAutoDisposeNotifier<List<SyncedItem>> { extends BuildlessAutoDisposeNotifier<List<SyncedItem>> {
late final SyncedItem arg; late final SyncedItem root;
List<SyncedItem> build( List<SyncedItem> build(
SyncedItem arg, SyncedItem root,
); );
} }
@ -49,10 +49,10 @@ class SyncChildrenFamily extends Family<List<SyncedItem>> {
/// See also [SyncChildren]. /// See also [SyncChildren].
SyncChildrenProvider call( SyncChildrenProvider call(
SyncedItem arg, SyncedItem root,
) { ) {
return SyncChildrenProvider( return SyncChildrenProvider(
arg, root,
); );
} }
@ -61,7 +61,7 @@ class SyncChildrenFamily extends Family<List<SyncedItem>> {
covariant SyncChildrenProvider provider, covariant SyncChildrenProvider provider,
) { ) {
return call( return call(
provider.arg, provider.root,
); );
} }
@ -85,9 +85,9 @@ class SyncChildrenProvider
extends AutoDisposeNotifierProviderImpl<SyncChildren, List<SyncedItem>> { extends AutoDisposeNotifierProviderImpl<SyncChildren, List<SyncedItem>> {
/// See also [SyncChildren]. /// See also [SyncChildren].
SyncChildrenProvider( SyncChildrenProvider(
SyncedItem arg, SyncedItem root,
) : this._internal( ) : this._internal(
() => SyncChildren()..arg = arg, () => SyncChildren()..root = root,
from: syncChildrenProvider, from: syncChildrenProvider,
name: r'syncChildrenProvider', name: r'syncChildrenProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -97,7 +97,7 @@ class SyncChildrenProvider
dependencies: SyncChildrenFamily._dependencies, dependencies: SyncChildrenFamily._dependencies,
allTransitiveDependencies: allTransitiveDependencies:
SyncChildrenFamily._allTransitiveDependencies, SyncChildrenFamily._allTransitiveDependencies,
arg: arg, root: root,
); );
SyncChildrenProvider._internal( SyncChildrenProvider._internal(
@ -107,17 +107,17 @@ class SyncChildrenProvider
required super.allTransitiveDependencies, required super.allTransitiveDependencies,
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.arg, required this.root,
}) : super.internal(); }) : super.internal();
final SyncedItem arg; final SyncedItem root;
@override @override
List<SyncedItem> runNotifierBuild( List<SyncedItem> runNotifierBuild(
covariant SyncChildren notifier, covariant SyncChildren notifier,
) { ) {
return notifier.build( return notifier.build(
arg, root,
); );
} }
@ -126,13 +126,13 @@ class SyncChildrenProvider
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SyncChildrenProvider._internal( override: SyncChildrenProvider._internal(
() => create()..arg = arg, () => create()..root = root,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
arg: arg, root: root,
), ),
); );
} }
@ -145,13 +145,13 @@ class SyncChildrenProvider
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SyncChildrenProvider && other.arg == arg; return other is SyncChildrenProvider && other.root == root;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode); hash = _SystemHash.combine(hash, root.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@ -160,8 +160,8 @@ class SyncChildrenProvider
@Deprecated('Will be removed in 3.0. Use Ref instead') @Deprecated('Will be removed in 3.0. Use Ref instead')
// ignore: unused_element // ignore: unused_element
mixin SyncChildrenRef on AutoDisposeNotifierProviderRef<List<SyncedItem>> { mixin SyncChildrenRef on AutoDisposeNotifierProviderRef<List<SyncedItem>> {
/// The parameter `arg` of this provider. /// The parameter `root` of this provider.
SyncedItem get arg; SyncedItem get root;
} }
class _SyncChildrenProviderElement class _SyncChildrenProviderElement
@ -170,18 +170,20 @@ class _SyncChildrenProviderElement
_SyncChildrenProviderElement(super.provider); _SyncChildrenProviderElement(super.provider);
@override @override
SyncedItem get arg => (origin as SyncChildrenProvider).arg; SyncedItem get root => (origin as SyncChildrenProvider).root;
} }
String _$syncDownloadStatusHash() => String _$syncDownloadStatusHash() =>
r'5a0f8537a977c52e6083bd84265631ea5d160637'; r'1036352200e1138b4ef70e524c0baf13bb9cd452';
abstract class _$SyncDownloadStatus abstract class _$SyncDownloadStatus
extends BuildlessAutoDisposeNotifier<DownloadStream?> { extends BuildlessAutoDisposeNotifier<DownloadStream?> {
late final SyncedItem arg; late final SyncedItem arg;
late final List<SyncedItem> children;
DownloadStream? build( DownloadStream? build(
SyncedItem arg, SyncedItem arg,
List<SyncedItem> children,
); );
} }
@ -197,9 +199,11 @@ class SyncDownloadStatusFamily extends Family<DownloadStream?> {
/// See also [SyncDownloadStatus]. /// See also [SyncDownloadStatus].
SyncDownloadStatusProvider call( SyncDownloadStatusProvider call(
SyncedItem arg, SyncedItem arg,
List<SyncedItem> children,
) { ) {
return SyncDownloadStatusProvider( return SyncDownloadStatusProvider(
arg, arg,
children,
); );
} }
@ -209,6 +213,7 @@ class SyncDownloadStatusFamily extends Family<DownloadStream?> {
) { ) {
return call( return call(
provider.arg, provider.arg,
provider.children,
); );
} }
@ -233,8 +238,11 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
/// See also [SyncDownloadStatus]. /// See also [SyncDownloadStatus].
SyncDownloadStatusProvider( SyncDownloadStatusProvider(
SyncedItem arg, SyncedItem arg,
List<SyncedItem> children,
) : this._internal( ) : this._internal(
() => SyncDownloadStatus()..arg = arg, () => SyncDownloadStatus()
..arg = arg
..children = children,
from: syncDownloadStatusProvider, from: syncDownloadStatusProvider,
name: r'syncDownloadStatusProvider', name: r'syncDownloadStatusProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -245,6 +253,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
allTransitiveDependencies: allTransitiveDependencies:
SyncDownloadStatusFamily._allTransitiveDependencies, SyncDownloadStatusFamily._allTransitiveDependencies,
arg: arg, arg: arg,
children: children,
); );
SyncDownloadStatusProvider._internal( SyncDownloadStatusProvider._internal(
@ -255,9 +264,11 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.arg, required this.arg,
required this.children,
}) : super.internal(); }) : super.internal();
final SyncedItem arg; final SyncedItem arg;
final List<SyncedItem> children;
@override @override
DownloadStream? runNotifierBuild( DownloadStream? runNotifierBuild(
@ -265,6 +276,7 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
) { ) {
return notifier.build( return notifier.build(
arg, arg,
children,
); );
} }
@ -273,13 +285,16 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SyncDownloadStatusProvider._internal( override: SyncDownloadStatusProvider._internal(
() => create()..arg = arg, () => create()
..arg = arg
..children = children,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
arg: arg, arg: arg,
children: children,
), ),
); );
} }
@ -292,13 +307,16 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SyncDownloadStatusProvider && other.arg == arg; return other is SyncDownloadStatusProvider &&
other.arg == arg &&
other.children == children;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode); hash = _SystemHash.combine(hash, arg.hashCode);
hash = _SystemHash.combine(hash, children.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@ -309,6 +327,9 @@ class SyncDownloadStatusProvider extends AutoDisposeNotifierProviderImpl<
mixin SyncDownloadStatusRef on AutoDisposeNotifierProviderRef<DownloadStream?> { mixin SyncDownloadStatusRef on AutoDisposeNotifierProviderRef<DownloadStream?> {
/// The parameter `arg` of this provider. /// The parameter `arg` of this provider.
SyncedItem get arg; SyncedItem get arg;
/// The parameter `children` of this provider.
List<SyncedItem> get children;
} }
class _SyncDownloadStatusProviderElement class _SyncDownloadStatusProviderElement
@ -318,16 +339,21 @@ class _SyncDownloadStatusProviderElement
@override @override
SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg; SyncedItem get arg => (origin as SyncDownloadStatusProvider).arg;
@override
List<SyncedItem> get children =>
(origin as SyncDownloadStatusProvider).children;
} }
String _$syncStatusesHash() => r'f05ee53368d1de130714bba09132e08aba15bc44'; String _$syncStatusesHash() => r'64a3499fc7b7bbdbd6594b1eec76cf42a119a041';
abstract class _$SyncStatuses abstract class _$SyncStatuses
extends BuildlessAutoDisposeAsyncNotifier<SyncStatus> { extends BuildlessAutoDisposeAsyncNotifier<SyncStatus> {
late final SyncedItem arg; late final SyncedItem arg;
late final List<SyncedItem>? children;
FutureOr<SyncStatus> build( FutureOr<SyncStatus> build(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
); );
} }
@ -343,9 +369,11 @@ class SyncStatusesFamily extends Family<AsyncValue<SyncStatus>> {
/// See also [SyncStatuses]. /// See also [SyncStatuses].
SyncStatusesProvider call( SyncStatusesProvider call(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
) { ) {
return SyncStatusesProvider( return SyncStatusesProvider(
arg, arg,
children,
); );
} }
@ -355,6 +383,7 @@ class SyncStatusesFamily extends Family<AsyncValue<SyncStatus>> {
) { ) {
return call( return call(
provider.arg, provider.arg,
provider.children,
); );
} }
@ -379,8 +408,11 @@ class SyncStatusesProvider
/// See also [SyncStatuses]. /// See also [SyncStatuses].
SyncStatusesProvider( SyncStatusesProvider(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
) : this._internal( ) : this._internal(
() => SyncStatuses()..arg = arg, () => SyncStatuses()
..arg = arg
..children = children,
from: syncStatusesProvider, from: syncStatusesProvider,
name: r'syncStatusesProvider', name: r'syncStatusesProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -391,6 +423,7 @@ class SyncStatusesProvider
allTransitiveDependencies: allTransitiveDependencies:
SyncStatusesFamily._allTransitiveDependencies, SyncStatusesFamily._allTransitiveDependencies,
arg: arg, arg: arg,
children: children,
); );
SyncStatusesProvider._internal( SyncStatusesProvider._internal(
@ -401,9 +434,11 @@ class SyncStatusesProvider
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.arg, required this.arg,
required this.children,
}) : super.internal(); }) : super.internal();
final SyncedItem arg; final SyncedItem arg;
final List<SyncedItem>? children;
@override @override
FutureOr<SyncStatus> runNotifierBuild( FutureOr<SyncStatus> runNotifierBuild(
@ -411,6 +446,7 @@ class SyncStatusesProvider
) { ) {
return notifier.build( return notifier.build(
arg, arg,
children,
); );
} }
@ -419,13 +455,16 @@ class SyncStatusesProvider
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SyncStatusesProvider._internal( override: SyncStatusesProvider._internal(
() => create()..arg = arg, () => create()
..arg = arg
..children = children,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
arg: arg, arg: arg,
children: children,
), ),
); );
} }
@ -438,13 +477,16 @@ class SyncStatusesProvider
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SyncStatusesProvider && other.arg == arg; return other is SyncStatusesProvider &&
other.arg == arg &&
other.children == children;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode); hash = _SystemHash.combine(hash, arg.hashCode);
hash = _SystemHash.combine(hash, children.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@ -455,6 +497,9 @@ class SyncStatusesProvider
mixin SyncStatusesRef on AutoDisposeAsyncNotifierProviderRef<SyncStatus> { mixin SyncStatusesRef on AutoDisposeAsyncNotifierProviderRef<SyncStatus> {
/// The parameter `arg` of this provider. /// The parameter `arg` of this provider.
SyncedItem get arg; SyncedItem get arg;
/// The parameter `children` of this provider.
List<SyncedItem>? get children;
} }
class _SyncStatusesProviderElement class _SyncStatusesProviderElement
@ -464,15 +509,19 @@ class _SyncStatusesProviderElement
@override @override
SyncedItem get arg => (origin as SyncStatusesProvider).arg; SyncedItem get arg => (origin as SyncStatusesProvider).arg;
@override
List<SyncedItem>? get children => (origin as SyncStatusesProvider).children;
} }
String _$syncSizeHash() => r'138702f2dd69ab28d142bab67ab4a497bb24f252'; String _$syncSizeHash() => r'81797ecc4a6f600691b6f1fe0c16bae0228ec920';
abstract class _$SyncSize extends BuildlessAutoDisposeNotifier<int?> { abstract class _$SyncSize extends BuildlessAutoDisposeNotifier<int?> {
late final SyncedItem arg; late final SyncedItem arg;
late final List<SyncedItem>? children;
int? build( int? build(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
); );
} }
@ -488,9 +537,11 @@ class SyncSizeFamily extends Family<int?> {
/// See also [SyncSize]. /// See also [SyncSize].
SyncSizeProvider call( SyncSizeProvider call(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
) { ) {
return SyncSizeProvider( return SyncSizeProvider(
arg, arg,
children,
); );
} }
@ -500,6 +551,7 @@ class SyncSizeFamily extends Family<int?> {
) { ) {
return call( return call(
provider.arg, provider.arg,
provider.children,
); );
} }
@ -523,8 +575,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
/// See also [SyncSize]. /// See also [SyncSize].
SyncSizeProvider( SyncSizeProvider(
SyncedItem arg, SyncedItem arg,
List<SyncedItem>? children,
) : this._internal( ) : this._internal(
() => SyncSize()..arg = arg, () => SyncSize()
..arg = arg
..children = children,
from: syncSizeProvider, from: syncSizeProvider,
name: r'syncSizeProvider', name: r'syncSizeProvider',
debugGetCreateSourceHash: debugGetCreateSourceHash:
@ -534,6 +589,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
dependencies: SyncSizeFamily._dependencies, dependencies: SyncSizeFamily._dependencies,
allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies, allTransitiveDependencies: SyncSizeFamily._allTransitiveDependencies,
arg: arg, arg: arg,
children: children,
); );
SyncSizeProvider._internal( SyncSizeProvider._internal(
@ -544,9 +600,11 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
required super.debugGetCreateSourceHash, required super.debugGetCreateSourceHash,
required super.from, required super.from,
required this.arg, required this.arg,
required this.children,
}) : super.internal(); }) : super.internal();
final SyncedItem arg; final SyncedItem arg;
final List<SyncedItem>? children;
@override @override
int? runNotifierBuild( int? runNotifierBuild(
@ -554,6 +612,7 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
) { ) {
return notifier.build( return notifier.build(
arg, arg,
children,
); );
} }
@ -562,13 +621,16 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
return ProviderOverride( return ProviderOverride(
origin: this, origin: this,
override: SyncSizeProvider._internal( override: SyncSizeProvider._internal(
() => create()..arg = arg, () => create()
..arg = arg
..children = children,
from: from, from: from,
name: null, name: null,
dependencies: null, dependencies: null,
allTransitiveDependencies: null, allTransitiveDependencies: null,
debugGetCreateSourceHash: null, debugGetCreateSourceHash: null,
arg: arg, arg: arg,
children: children,
), ),
); );
} }
@ -580,13 +642,16 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
@override @override
bool operator ==(Object other) { bool operator ==(Object other) {
return other is SyncSizeProvider && other.arg == arg; return other is SyncSizeProvider &&
other.arg == arg &&
other.children == children;
} }
@override @override
int get hashCode { int get hashCode {
var hash = _SystemHash.combine(0, runtimeType.hashCode); var hash = _SystemHash.combine(0, runtimeType.hashCode);
hash = _SystemHash.combine(hash, arg.hashCode); hash = _SystemHash.combine(hash, arg.hashCode);
hash = _SystemHash.combine(hash, children.hashCode);
return _SystemHash.finish(hash); return _SystemHash.finish(hash);
} }
@ -597,6 +662,9 @@ class SyncSizeProvider extends AutoDisposeNotifierProviderImpl<SyncSize, int?> {
mixin SyncSizeRef on AutoDisposeNotifierProviderRef<int?> { mixin SyncSizeRef on AutoDisposeNotifierProviderRef<int?> {
/// The parameter `arg` of this provider. /// The parameter `arg` of this provider.
SyncedItem get arg; SyncedItem get arg;
/// The parameter `children` of this provider.
List<SyncedItem>? get children;
} }
class _SyncSizeProviderElement class _SyncSizeProviderElement
@ -606,6 +674,8 @@ class _SyncSizeProviderElement
@override @override
SyncedItem get arg => (origin as SyncSizeProvider).arg; SyncedItem get arg => (origin as SyncSizeProvider).arg;
@override
List<SyncedItem>? get children => (origin as SyncSizeProvider).children;
} }
// ignore_for_file: type=lint // ignore_for_file: type=lint
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package // ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package

View file

@ -22,6 +22,7 @@ import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/images_models.dart'; import 'package:fladder/models/items/images_models.dart';
import 'package:fladder/models/items/media_streams_model.dart'; import 'package:fladder/models/items/media_streams_model.dart';
import 'package:fladder/models/items/movie_model.dart'; import 'package:fladder/models/items/movie_model.dart';
import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/models/items/series_model.dart';
import 'package:fladder/models/items/trick_play_model.dart'; import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/download_stream.dart'; import 'package:fladder/models/syncing/download_stream.dart';
@ -238,6 +239,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
fladderSnackbar(context, title: context.localized.syncAddItemForSyncing(item.detailedName(context) ?? "Unknown")); fladderSnackbar(context, title: context.localized.syncAddItemForSyncing(item.detailedName(context) ?? "Unknown"));
final newSync = switch (item) { final newSync = switch (item) {
EpisodeModel episode => await syncSeries(item.parentBaseModel, episode: episode), EpisodeModel episode => await syncSeries(item.parentBaseModel, episode: episode),
SeasonModel season => await syncSeries(item.parentBaseModel, season: season),
SeriesModel series => await syncSeries(series), SeriesModel series => await syncSeries(series),
MovieModel movie => await syncMovie(movie), MovieModel movie => await syncMovie(movie),
_ => null _ => null
@ -367,7 +369,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
if (data == null) return data; if (data == null) return data;
if (!itemPath.existsSync()) return data; if (!itemPath.existsSync()) return data;
if (data.isEmpty) return data; if (data.isEmpty) return data;
final saveDirectory = Directory(path.joinAll([itemPath.path, "Chapters"])); final saveDirectory = Directory(path.joinAll([itemPath.path, SyncedItem.chaptersPath]));
await saveDirectory.create(recursive: true); await saveDirectory.create(recursive: true);
@ -378,7 +380,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
if (response.bodyBytes.isEmpty) return null; if (response.bodyBytes.isEmpty) return null;
file.writeAsBytesSync(response.bodyBytes); file.writeAsBytesSync(response.bodyBytes);
return event.copyWith( return event.copyWith(
imageUrl: path.joinAll(["Chapters", fileName]), imageUrl: path.joinAll([SyncedItem.chaptersPath, fileName]),
); );
}).toList(); }).toList();
return saveChapters.nonNulls.toList(); return saveChapters.nonNulls.toList();
@ -415,7 +417,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
return syncedItem; return syncedItem;
} }
Future<DownloadStream?> syncVideoFile(SyncedItem syncItem, bool skipDownload) async { Future<DownloadStream?> syncFile(SyncedItem syncItem, bool skipDownload) async {
cleanupTemporaryFiles(); cleanupTemporaryFiles();
final playbackResponse = await api.itemsItemIdPlaybackInfoPost( final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
@ -439,6 +441,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
final mediaSegments = (await api.mediaSegmentsGet(id: syncItem.id))?.body; final mediaSegments = (await api.mediaSegmentsGet(id: syncItem.id))?.body;
syncItem = syncItem.copyWith( syncItem = syncItem.copyWith(
fChapters: await saveChapterImages(item?.overview.chapters, directory) ?? [],
subtitles: subtitles, subtitles: subtitles,
fTrickPlayModel: trickPlayFile, fTrickPlayModel: trickPlayFile,
mediaSegments: mediaSegments, mediaSegments: mediaSegments,
@ -447,23 +450,24 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
await updateItem(syncItem); await updateItem(syncItem);
final currentTask = ref.read(downloadTasksProvider(syncItem.id)); final currentTask = ref.read(downloadTasksProvider(syncItem.id));
final user = ref.read(userProvider);
final downloadString = path.joinAll([ if (user == null) return null;
"${ref.read(userProvider)?.server}",
"Items", final downloadUrl = path.joinAll([user.server, "Items", syncItem.id, "Download"]);
"${syncItem.id}/Download?api_key=${ref.read(userProvider)?.credentials.token}"
]);
try { try {
if (!skipDownload && currentTask.task == null) { if (!skipDownload && currentTask.task == null) {
final downloadTask = DownloadTask( final downloadTask = DownloadTask(
url: Uri.parse(downloadString).toString(), url: Uri.parse(downloadUrl).toString(),
directory: syncItem.directory.path, directory: syncItem.directory.path,
filename: syncItem.videoFileName, filename: syncItem.videoFileName,
updates: Updates.statusAndProgress, updates: Updates.statusAndProgress,
baseDirectory: BaseDirectory.root, baseDirectory: BaseDirectory.root,
urlQueryParameters: {"api_key": user.credentials.token},
headers: user.credentials.header(ref),
requiresWiFi: ref.read(clientSettingsProvider.select((value) => value.requireWifi)), requiresWiFi: ref.read(clientSettingsProvider.select((value) => value.requireWifi)),
retries: 5, retries: 3,
allowPause: true, allowPause: true,
); );
@ -490,7 +494,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
(state) => state.copyWith(status: status), (state) => state.copyWith(status: status),
); );
if (status == TaskStatus.complete) { if (status == TaskStatus.complete || status == TaskStatus.canceled) {
ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => DownloadStream.empty()); ref.read(downloadTasksProvider(syncItem.id).notifier).update((state) => DownloadStream.empty());
} }
}, },
@ -522,6 +526,10 @@ extension SyncNotifierHelpers on SyncNotifier {
Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async { Future<SyncedItem> createSyncItem(BaseItemDto response, {SyncedItem? parent}) async {
final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref); final ItemBaseModel item = ItemBaseModel.fromBaseDto(response, ref);
final existingSyncedItem = getSyncedItem(item);
if (existingSyncedItem != null) return existingSyncedItem;
final Directory? parentDirectory = parent?.directory; final Directory? parentDirectory = parent?.directory;
final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id])); final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id]));
@ -543,31 +551,17 @@ extension SyncNotifierHelpers on SyncNotifier {
userData: item.userData, userData: item.userData,
); );
//Save item if parent so the user is aware.
if (parent == null) { if (parent == null) {
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath))); isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
} }
final origChapters = Chapter.chaptersFromInfo(item.id, response.chapters ?? [], ref);
return syncItem.copyWith( return syncItem.copyWith(
fChapters: await saveChapterImages(origChapters, directory) ?? [],
fileSize: response.mediaSources?.firstOrNull?.size ?? 0, fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
syncing: false, syncing: false,
videoFileName: response.path?.split('/').lastOrNull ?? "", videoFileName: response.path?.split('/').lastOrNull ?? "",
); );
} }
// Need to move the file after downloading on Android
Future<void> moveFile(DownloadTask downloadTask, SyncedItem syncItem) async {
final currentLocation = File(await downloadTask.filePath());
final wantedLocation = syncItem.videoFile;
if (currentLocation.path != wantedLocation.path) {
await currentLocation.copy(wantedLocation.path);
await currentLocation.delete();
}
}
Future<SyncedItem?> syncMovie(ItemBaseModel item, {bool skipDownload = false}) async { Future<SyncedItem?> syncMovie(ItemBaseModel item, {bool skipDownload = false}) async {
final response = await api.usersUserIdItemsItemIdGetBaseItem( final response = await api.usersUserIdItemsItemIdGetBaseItem(
itemId: item.id, itemId: item.id,
@ -580,21 +574,21 @@ extension SyncNotifierHelpers on SyncNotifier {
if (!syncItem.directory.existsSync()) return null; if (!syncItem.directory.existsSync()) return null;
await syncVideoFile(syncItem, skipDownload); await syncFile(syncItem, skipDownload);
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath))); isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
return syncItem; return syncItem;
} }
Future<SyncedItem?> syncSeries(SeriesModel item, {EpisodeModel? episode}) async { Future<SyncedItem?> syncSeries(SeriesModel item, {SeasonModel? season, EpisodeModel? episode}) async {
final response = await api.usersUserIdItemsItemIdGetBaseItem( final response = await api.usersUserIdItemsItemIdGetBaseItem(
itemId: item.id, itemId: item.id,
); );
List<SyncedItem> newItems = []; List<SyncedItem> newItems = [];
SyncedItem? itemToDownload; List<SyncedItem>? itemsToDownload = [];
SyncedItem seriesItem = await createSyncItem(response.bodyOrThrow); SyncedItem seriesItem = await createSyncItem(response.bodyOrThrow);
newItems.add(seriesItem); newItems.add(seriesItem);
@ -627,8 +621,8 @@ extension SyncNotifierHelpers on SyncNotifier {
final seasons = seasonsResponse.body?.items ?? []; final seasons = seasonsResponse.body?.items ?? [];
for (var i = 0; i < seasons.length; i++) { for (var i = 0; i < seasons.length; i++) {
final season = seasons[i]; final newSeason = seasons[i];
final syncedSeason = await createSyncItem(season, parent: seriesItem); final syncedSeason = await createSyncItem(newSeason, parent: seriesItem);
newItems.add(syncedSeason); newItems.add(syncedSeason);
final episodesResponse = await api.showsSeriesIdEpisodesGet( final episodesResponse = await api.showsSeriesIdEpisodesGet(
isMissing: false, isMissing: false,
@ -651,16 +645,23 @@ extension SyncNotifierHelpers on SyncNotifier {
ItemFields.chapters, ItemFields.chapters,
ItemFields.trickplay, ItemFields.trickplay,
], ],
seasonId: season.id, seasonId: newSeason.id,
seriesId: seriesItem.id, seriesId: seriesItem.id,
); );
final episodes = episodesResponse.body?.items ?? []; final episodes = episodesResponse.body?.items ?? [];
for (var i = 0; i < episodes.length; i++) {
final item = episodes[i]; final episodeResults = await Future.wait(
final newEpisode = await createSyncItem(item, parent: syncedSeason); episodes.map((ep) async {
final newEpisode = await createSyncItem(ep, parent: syncedSeason);
return (ep, newEpisode);
}),
);
for (final (ep, newEpisode) in episodeResults) {
newItems.add(newEpisode); newItems.add(newEpisode);
if (episode?.id == item.id) { if (episode?.id == ep.id || newSeason.id == season?.id) {
itemToDownload = newEpisode; itemsToDownload.add(newEpisode);
} }
} }
} }
@ -673,8 +674,9 @@ extension SyncNotifierHelpers on SyncNotifier {
.toList()), .toList()),
); );
if (itemToDownload != null) { for (var i = 0; i < itemsToDownload.length; i++) {
await syncVideoFile(itemToDownload, false); final item = itemsToDownload[i];
await syncFile(item, false);
} }
return seriesItem; return seriesItem;

View file

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

View file

@ -206,7 +206,7 @@ class EpisodePoster extends ConsumerWidget {
if (iSyncedItem != null) if (iSyncedItem != null)
Consumer(builder: (context, ref, child) { Consumer(builder: (context, ref, child) {
final SyncStatus syncStatus = final SyncStatus syncStatus =
ref.watch(syncStatusesProvider(iSyncedItem)).value ?? SyncStatus.partially; ref.watch(syncStatusesProvider(iSyncedItem, null)).value ?? SyncStatus.partially;
return StatusCard( return StatusCard(
color: syncStatus.color, color: syncStatus.color,
child: SyncButton(item: episode, syncedItem: syncedItem), child: SyncButton(item: episode, syncedItem: syncedItem),

View file

@ -3,7 +3,9 @@ import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/flat_button.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/syncing/sync_button.dart';
import 'package:fladder/util/adaptive_layout/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout/adaptive_layout.dart';
import 'package:fladder/util/disable_keypad_focus.dart'; import 'package:fladder/util/disable_keypad_focus.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
@ -56,6 +58,7 @@ class SeasonPoster extends ConsumerWidget {
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final syncedItem = ref.watch(syncProvider.notifier).getSyncedItem(season);
Padding placeHolder(String title) { Padding placeHolder(String title) {
return Padding( return Padding(
padding: const EdgeInsets.all(4), padding: const EdgeInsets.all(4),
@ -100,15 +103,27 @@ class SeasonPoster extends ConsumerWidget {
if (season.userData.unPlayedItemCount != 0) if (season.userData.unPlayedItemCount != 0)
Align( Align(
alignment: Alignment.topRight, alignment: Alignment.topRight,
child: StatusCard( child: Row(
color: Theme.of(context).colorScheme.primary, mainAxisAlignment: MainAxisAlignment.end,
useFittedBox: true, children: [
child: Center( if (syncedItem != null)
child: Text( StatusCard(
season.userData.unPlayedItemCount.toString(), child: SyncButton(
style: const TextStyle(fontWeight: FontWeight.w700, fontSize: 14), item: season,
syncedItem: syncedItem,
),
),
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 else

View file

@ -1,12 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncButton extends ConsumerStatefulWidget { class SyncButton extends ConsumerStatefulWidget {
final ItemBaseModel item; final ItemBaseModel item;
@ -21,37 +20,21 @@ class _SyncButtonState extends ConsumerState<SyncButton> {
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final syncedItem = widget.syncedItem; final syncedItem = widget.syncedItem;
final status = syncedItem != null ? ref.watch(syncStatusesProvider(syncedItem)).value : null; final status = syncedItem != null ? ref.watch(syncStatusesProvider(syncedItem, null)).value : null;
final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem)) : null; final progress = syncedItem != null ? ref.watch(syncDownloadStatusProvider(syncedItem, [])) : null;
return Stack( return Stack(
alignment: Alignment.center, alignment: Alignment.center,
children: [ children: [
InkWell( Icon(
onTap: syncedItem != null syncedItem != null
? () => showSyncItemDetails(context, syncedItem, ref) ? status == SyncStatus.partially
: () => showDefaultActionDialog( ? (progress?.progress ?? 0) > 0
context, ? IconsaxPlusLinear.arrow_down
'Sync ${widget.item.detailedName}?', : IconsaxPlusLinear.more_circle
null, : IconsaxPlusLinear.tick_circle
(context) async { : IconsaxPlusLinear.arrow_down_2,
await ref.read(syncProvider.notifier).addSyncItem(context, widget.item); color: status?.color,
Navigator.of(context).pop(); size: (progress?.progress ?? 0) > 0 ? 16 : null,
},
"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) if ((progress?.progress ?? 0) > 0)
IgnorePointer( IgnorePointer(

View file

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

View file

@ -1,13 +1,11 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/settings/client_settings_provider.dart'; import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/adaptive_dialog.dart'; import 'package:fladder/screens/shared/adaptive_dialog.dart';
@ -101,6 +99,7 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
Expanded( Expanded(
child: SyncProgressBuilder( child: SyncProgressBuilder(
item: syncedItem, item: syncedItem,
children: syncChildren,
builder: (context, combinedStream) { builder: (context, combinedStream) {
return Row( return Row(
children: [ children: [
@ -114,52 +113,17 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
), ),
SyncSubtitle(syncItem: syncedItem), SyncSubtitle(syncItem: syncedItem),
SyncLabel( SyncLabel(
label: context.localized label: context.localized.totalSize(
.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), ref.watch(syncSizeProvider(syncedItem, syncChildren)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, status: ref.watch(syncStatusesProvider(syncedItem, syncChildren)).value ??
SyncStatus.partially,
), ),
if (combinedStream?.task != null && combinedStream != null) ...{
SyncProgressBar(item: syncedItem, task: combinedStream)
},
].addInBetween(const SizedBox(height: 8)), ].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)}%"))
],
)),
], ],
); );
}, },
@ -167,7 +131,7 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
), ),
if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile) if (!hasFile && !downloadTask.hasDownload && syncedItem.hasVideoFile)
IconButtonAwait( IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false), onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false),
icon: const Icon(IconsaxPlusLinear.cloud_change), icon: const Icon(IconsaxPlusLinear.cloud_change),
) )
else if (hasFile) else if (hasFile)

View file

@ -29,6 +29,7 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final syncedItem = widget.syncedItem; final syncedItem = widget.syncedItem;
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
final children = ref.watch(syncChildrenProvider(syncedItem));
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: SyncStatusOverlay( child: SyncStatusOverlay(
@ -89,6 +90,7 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
Expanded( Expanded(
child: SyncProgressBuilder( child: SyncProgressBuilder(
item: syncedItem, item: syncedItem,
children: children,
builder: (context, combinedStream) { builder: (context, combinedStream) {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
@ -103,13 +105,17 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
), ),
), ),
Flexible( Flexible(
child: SyncSubtitle(syncItem: syncedItem), child: SyncSubtitle(
syncItem: syncedItem,
children: children,
),
), ),
Flexible( Flexible(
child: SyncLabel( child: SyncLabel(
label: context.localized label: context.localized.totalSize(
.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), ref.watch(syncSizeProvider(syncedItem, children)).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, status: ref.watch(syncStatusesProvider(syncedItem, children)).value ??
SyncStatus.partially,
), ),
), ),
if (combinedStream != null && combinedStream.hasDownload == true) if (combinedStream != null && combinedStream.hasDownload == true)

View file

@ -1,8 +1,8 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
@ -15,6 +15,13 @@ import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
const _cancellableStatuses = {
TaskStatus.canceled,
TaskStatus.failed,
TaskStatus.enqueued,
TaskStatus.waitingToRetry,
};
class SyncLabel extends ConsumerWidget { class SyncLabel extends ConsumerWidget {
final String? label; final String? label;
final SyncStatus status; final SyncStatus status;
@ -76,18 +83,24 @@ class SyncProgressBar extends ConsumerWidget {
IconButton( IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).pause(downloadTask), onPressed: () => ref.read(backgroundDownloaderProvider).pause(downloadTask),
icon: const Icon(IconsaxPlusBold.pause), icon: const Icon(IconsaxPlusBold.pause),
),
if (downloadStatus == TaskStatus.paused) ...[
IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask),
icon: const Icon(IconsaxPlusBold.play),
),
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
icon: const Icon(IconsaxPlusBold.stop),
) )
],
if (_cancellableStatuses.contains(downloadStatus)) ...[
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
icon: const Icon(IconsaxPlusBold.stop),
),
],
}, },
if (downloadStatus == TaskStatus.paused && downloadTask != null) ...[
IconButton(
onPressed: () => ref.read(backgroundDownloaderProvider).resume(downloadTask),
icon: const Icon(IconsaxPlusBold.play),
),
IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
icon: const Icon(IconsaxPlusBold.stop),
)
],
].addInBetween(const SizedBox(width: 8)), ].addInBetween(const SizedBox(width: 8)),
), ),
const SizedBox(width: 6), const SizedBox(width: 6),
@ -98,16 +111,17 @@ class SyncProgressBar extends ConsumerWidget {
class SyncSubtitle extends ConsumerWidget { class SyncSubtitle extends ConsumerWidget {
final SyncedItem syncItem; final SyncedItem syncItem;
final List<SyncedItem> children;
const SyncSubtitle({ const SyncSubtitle({
required this.syncItem, required this.syncItem,
this.children = const [],
super.key, super.key,
}); });
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
final baseItem = ref.read(syncProvider.notifier).getItem(syncItem); final baseItem = ref.read(syncProvider.notifier).getItem(syncItem);
final children = syncItem.nestedChildren(ref); final syncStatus = ref.watch(syncStatusesProvider(syncItem, children)).value ?? SyncStatus.partially;
final syncStatus = ref.watch(syncStatusesProvider(syncItem)).value ?? SyncStatus.partially;
return Container( return Container(
decoration: decoration:
BoxDecoration(color: syncStatus.color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)), BoxDecoration(color: syncStatus.color.withValues(alpha: 0.15), borderRadius: BorderRadius.circular(10)),

View file

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

View file

@ -1,13 +1,15 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:iconsax_plus/iconsax_plus.dart'; import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/default_alert_dialog.dart'; import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/shared/media/episode_posters.dart'; import 'package:fladder/screens/shared/media/episode_posters.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
@ -40,11 +42,15 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
return Row( return Row(
children: [ children: [
IgnorePointer( ConstrainedBox(
child: ConstrainedBox( constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3),
constraints: BoxConstraints(maxWidth: MediaQuery.of(context).size.width * 0.3), child: FlatButton(
onTap: () {
widget.episode.navigateTo(context);
return context.maybePop();
},
child: SizedBox( child: SizedBox(
width: 250, width: 175,
child: EpisodePoster( child: EpisodePoster(
episode: widget.episode, episode: widget.episode,
syncedItem: syncedItem, syncedItem: syncedItem,
@ -87,8 +93,8 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
else else
Flexible( Flexible(
child: SyncLabel( child: SyncLabel(
label: context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem)).byteFormat ?? '--'), label: context.localized.totalSize(ref.watch(syncSizeProvider(syncedItem, [])).byteFormat ?? '--'),
status: ref.watch(syncStatusesProvider(syncedItem)).value ?? SyncStatus.partially, status: ref.watch(syncStatusesProvider(syncedItem, [])).value ?? SyncStatus.partially,
), ),
) )
], ],
@ -96,7 +102,7 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
), ),
if (!hasFile && !downloadTask.hasDownload) if (!hasFile && !downloadTask.hasDownload)
IconButtonAwait( IconButtonAwait(
onPressed: () async => await ref.read(syncProvider.notifier).syncVideoFile(syncedItem, false), onPressed: () async => await ref.read(syncProvider.notifier).syncFile(syncedItem, false),
icon: const Icon(IconsaxPlusLinear.cloud_change), icon: const Icon(IconsaxPlusLinear.cloud_change),
) )
else if (hasFile) else if (hasFile)

View file

@ -1,13 +1,17 @@
import 'package:flutter/material.dart';
import 'package:auto_route/auto_route.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:iconsax_plus/iconsax_plus.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/screens/shared/animated_fade_size.dart'; import 'package:fladder/screens/shared/flat_button.dart';
import 'package:fladder/screens/syncing/widgets/synced_episode_item.dart'; import 'package:fladder/screens/syncing/widgets/synced_episode_item.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/widgets/shared/icon_button_await.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncedSeasonPoster extends ConsumerStatefulWidget { class SyncedSeasonPoster extends ConsumerStatefulWidget {
const SyncedSeasonPoster({ const SyncedSeasonPoster({
@ -29,14 +33,21 @@ class _SyncedSeasonPosterState extends ConsumerState<SyncedSeasonPoster> {
Widget build(BuildContext context) { Widget build(BuildContext context) {
final season = widget.season; final season = widget.season;
final children = ref.read(syncProvider.notifier).getChildren(widget.syncedItem); final children = ref.read(syncProvider.notifier).getChildren(widget.syncedItem);
return Column( final unSyncedChildren = children.where((child) => child.status == SyncStatus.partially).toList();
children: [ return ExpansionTile(
Row( tilePadding: EdgeInsets.zero,
children: [ title: Row(
SizedBox( spacing: 6,
width: 125, children: [
child: AspectRatio( SizedBox(
aspectRatio: 0.65, width: 75,
child: AspectRatio(
aspectRatio: 0.65,
child: FlatButton(
onTap: () {
season.navigateTo(context);
return context.maybePop();
},
child: Card( child: Card(
child: FladderImage( child: FladderImage(
image: season.getPosters?.primary ?? image: season.getPosters?.primary ??
@ -46,50 +57,47 @@ class _SyncedSeasonPosterState extends ConsumerState<SyncedSeasonPoster> {
), ),
), ),
), ),
Column( ),
children: [ Column(
Text( children: [
season.name, Text(
style: Theme.of(context).textTheme.titleMedium, season.name,
) style: Theme.of(context).textTheme.titleMedium,
], )
), ],
const Spacer(), ),
IconButton( ],
onPressed: () { ),
setState(() { trailing: Row(
expanded = !expanded; mainAxisSize: MainAxisSize.min,
}); children: [
if (unSyncedChildren.isNotEmpty)
IconButtonAwait(
onPressed: () async {
for (var i = 0; i < unSyncedChildren.length; i++) {
final childSyncedItem = unSyncedChildren[i];
await ref.read(syncProvider.notifier).syncFile(childSyncedItem, false);
}
}, },
icon: Icon(!expanded ? Icons.keyboard_arrow_down_rounded : Icons.keyboard_arrow_up_rounded), icon: const Icon(IconsaxPlusLinear.cloud_change),
) ),
].addPadding(const EdgeInsets.symmetric(horizontal: 6)), ],
), ),
AnimatedFadeSize( children: children.map(
duration: const Duration(milliseconds: 250), (item) {
child: expanded && children.isNotEmpty final baseItem = ref.read(syncProvider.notifier).getItem(item);
? ListView( return Padding(
shrinkWrap: true, padding: const EdgeInsets.symmetric(vertical: 8),
physics: const NeverScrollableScrollPhysics(), child: IntrinsicHeight(
children: <Widget>[ child: SyncedEpisodeItem(
const Divider(), episode: baseItem as EpisodeModel,
...children.map( syncedItem: item,
(item) { hasFile: item.videoFile.existsSync(),
final baseItem = ref.read(syncProvider.notifier).getItem(item); ),
return IntrinsicHeight( ),
child: SyncedEpisodeItem( );
episode: baseItem as EpisodeModel, },
syncedItem: item, ).toList(),
hasFile: item.videoFile.existsSync(),
),
);
},
)
].addPadding(const EdgeInsets.symmetric(vertical: 10)),
)
: Container(),
)
].addPadding(EdgeInsets.only(top: 10, bottom: expanded ? 10 : 0)),
); );
} }
} }

View file

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

View file

@ -9,6 +9,7 @@ import 'package:fladder/models/item_base_model.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/item_shared_models.dart'; import 'package:fladder/models/items/item_shared_models.dart';
import 'package:fladder/models/items/photos_model.dart'; import 'package:fladder/models/items/photos_model.dart';
import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/collections/add_to_collection.dart'; import 'package:fladder/screens/collections/add_to_collection.dart';
@ -243,8 +244,12 @@ extension ItemBaseModelExtensions on ItemBaseModel {
else else
ItemActionButton( ItemActionButton(
icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)), icon: IgnorePointer(child: SyncButton(item: this, syncedItem: syncedItem)),
action: () => showSyncItemDetails(context, syncedItem, ref), action: () => syncedItem.status == SyncStatus.complete
label: Text(context.localized.syncDetails), ? ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, null)
: ref.read(syncProvider.notifier).syncFile(syncedItem, false),
label: Text(
syncedItem.status == SyncStatus.complete ? context.localized.delete : context.localized.sync,
),
) )
else if (downloadUrl != null) ...[ else if (downloadUrl != null) ...[
ItemActionButton( ItemActionButton(

View file

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

View file

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

View file

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

View file

@ -514,10 +514,10 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: file_picker name: file_picker
sha256: "77f8e81d22d2a07d0dee2c62e1dda71dc1da73bf43bb2d45af09727406167964" sha256: ef9908739bdd9c476353d6adff72e88fd00c625f5b959ae23f7567bd5137db0a
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "10.1.9" version: "10.2.0"
fixnum: fixnum:
dependency: transitive dependency: transitive
description: description:
@ -942,18 +942,18 @@ packages:
dependency: "direct main" dependency: "direct main"
description: description:
name: isar name: isar
sha256: ebf74d87c400bd9f7da14acb31932b50c2407edbbd40930da3a6c2a8143f85a8 sha256: e987032e5d007a03ba4415cbf4d47add17b57ac664db8705db90fbfeb6a16737
url: "https://pub.dev" url: "https://pub.isar-community.dev"
source: hosted source: hosted
version: "4.0.0-dev.14" version: "4.0.3"
isar_flutter_libs: isar_flutter_libs:
dependency: "direct main" dependency: "direct main"
description: description:
name: isar_flutter_libs name: isar_flutter_libs
sha256: "04a3f4035e213ddb6e78d0132a7c80296a085c2088c2a761b4a42ee5add36983" sha256: a6b86d8618fe2d7d0e2ac6aa7a7f21c0c8ae912ccbef94a45d9f6e1e519ef610
url: "https://pub.dev" url: "https://pub.isar-community.dev"
source: hosted source: hosted
version: "4.0.0-dev.14" version: "4.0.3"
js: js:
dependency: transitive dependency: transitive
description: description:
@ -2027,10 +2027,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: vector_graphics name: vector_graphics
sha256: "44cc7104ff32563122a929e4620cf3efd584194eec6d1d913eb5ba593dbcf6de" sha256: a4f059dc26fc8295b5921376600a194c4ec7d55e72f2fe4c7d2831e103d461e6
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.18" version: "1.1.19"
vector_graphics_codec: vector_graphics_codec:
dependency: transitive dependency: transitive
description: description:
@ -2139,10 +2139,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: watcher name: watcher
sha256: "69da27e49efa56a15f8afe8f4438c4ec02eff0a117df1b22ea4aad194fe1c104" sha256: "0b7fd4a0bbc4b92641dbf20adfd7e3fd1398fe17102d94b674234563e110088a"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "1.1.1" version: "1.1.2"
weak_map: weak_map:
dependency: transitive dependency: transitive
description: description:
@ -2211,10 +2211,10 @@ packages:
dependency: transitive dependency: transitive
description: description:
name: win32 name: win32
sha256: "329edf97fdd893e0f1e3b9e88d6a0e627128cc17cc316a8d67fda8f1451178ba" sha256: "66814138c3562338d05613a6e368ed8cfb237ad6d64a9e9334be3f309acfca03"
url: "https://pub.dev" url: "https://pub.dev"
source: hosted source: hosted
version: "5.13.0" version: "5.14.0"
window_manager: window_manager:
dependency: "direct main" dependency: "direct main"
description: description:

View file

@ -98,7 +98,7 @@ dependencies:
# Utility # Utility
path: ^1.9.1 path: ^1.9.1
file_picker: ^10.1.9 file_picker: ^10.2.0
transparent_image: ^2.0.1 transparent_image: ^2.0.1
universal_html: ^2.2.4 universal_html: ^2.2.4
collection: ^1.19.1 collection: ^1.19.1
@ -114,8 +114,12 @@ dependencies:
screen_retriever: ^0.2.0 screen_retriever: ^0.2.0
# Data # Data
isar: ^4.0.0-dev.14 isar:
isar_flutter_libs: ^4.0.0-dev.14 # contains Isar Core version: ^4.0.3
hosted: https://pub.isar-community.dev/
isar_flutter_libs: # contains Isar Core
version: ^4.0.3
hosted: https://pub.isar-community.dev/
# Other # Other
async: ^2.13.0 async: ^2.13.0