fix: Sync leaving left over temp files (#73)

Co-authored-by: PartyDonut <PartyDonut@users.noreply.github.com>
This commit is contained in:
PartyDonut 2024-10-25 18:58:44 +02:00 committed by GitHub
parent c5e39db9ec
commit 8d15e319d3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 644 additions and 406 deletions

View file

@ -267,8 +267,8 @@
"@hideEmpty": {}, "@hideEmpty": {},
"home": "Home", "home": "Home",
"@home": {}, "@home": {},
"homeBannerBanner": "Banner", "homeBannerSlideshow": "Slideshow",
"@homeBannerBanner": {}, "@homeBannerSlideshow": {},
"homeBannerCarousel": "Carousel", "homeBannerCarousel": "Carousel",
"@homeBannerCarousel": {}, "@homeBannerCarousel": {},
"identify": "Identify", "identify": "Identify",
@ -947,5 +947,44 @@
"example": "1" "example": "1"
} }
} }
},
"syncStatusEnqueued": "Enqueued",
"syncStatusRunning": "Running",
"syncStatusComplete": "Complete",
"syncStatusNotFound": "Not Found",
"syncStatusFailed": "Failed",
"syncStatusCanceled": "Canceled",
"syncStatusWaitingToRetry": "Waiting to retry",
"syncStatusPaused": "Paused",
"syncStatusSynced": "Synced",
"syncStatusPartially": "Partially",
"syncOverlayDeleting": "Removing synced item",
"syncOverlaySyncing": "Syncing item details",
"syncSelectDownloadsFolder": "Select downloads folder",
"syncNoFolderSetup": "No sync folder setup",
"syncRemoveUnableToDeleteItem": "Unable to remove synced item, somethin went wrong",
"syncAddItemForSyncing": "Added {item} for syncing",
"@syncAddItemForSyncing":{
"placeholders": {
"item":{
"type": "String"
}
}
} ,
"startedSyncingItem": "Started syncing {item}",
"@startedSyncingItem": {
"placeholders": {
"item":{
"type": "String"
}
}
},
"unableToSyncItem": "Unable to sync {item}, something went wrong",
"@unableToSyncItem": {
"placeholders": {
"item":{
"type": "String"
}
}
} }
} }

View file

@ -28,7 +28,7 @@ enum HomeBanner {
String label(BuildContext context) => switch (this) { String label(BuildContext context) => switch (this) {
HomeBanner.hide => context.localized.hide, HomeBanner.hide => context.localized.hide,
HomeBanner.carousel => context.localized.homeBannerCarousel, HomeBanner.carousel => context.localized.homeBannerCarousel,
HomeBanner.banner => context.localized.homeBannerBanner, HomeBanner.banner => context.localized.homeBannerSlideshow,
}; };
} }

View file

@ -28,6 +28,7 @@ part 'i_synced_item.g.dart';
class ISyncedItem { class ISyncedItem {
String? userId; String? userId;
String id; String id;
bool syncing;
String? sortName; String? sortName;
String? parentId; String? parentId;
String? path; String? path;
@ -42,6 +43,7 @@ class ISyncedItem {
ISyncedItem({ ISyncedItem({
this.userId, this.userId,
required this.id, required this.id,
required this.syncing,
this.sortName, this.sortName,
this.parentId, this.parentId,
this.path, this.path,
@ -59,6 +61,7 @@ class ISyncedItem {
return ISyncedItem( return ISyncedItem(
id: syncedItem.id, id: syncedItem.id,
parentId: syncedItem.parentId, parentId: syncedItem.parentId,
syncing: syncedItem.syncing,
userId: syncedItem.userId, userId: syncedItem.userId,
path: syncedItem.path?.replaceAll(path ?? "", '').substring(1), path: syncedItem.path?.replaceAll(path ?? "", '').substring(1),
fileSize: syncedItem.fileSize, fileSize: syncedItem.fileSize,

File diff suppressed because it is too large Load diff

View file

@ -20,6 +20,7 @@ import 'package:fladder/models/items/trick_play_model.dart';
import 'package:fladder/models/syncing/i_synced_item.dart'; import 'package:fladder/models/syncing/i_synced_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/localization_helper.dart';
part 'sync_item.freezed.dart'; part 'sync_item.freezed.dart';
@ -29,6 +30,7 @@ class SyncedItem with _$SyncedItem {
factory SyncedItem({ factory SyncedItem({
required String id, required String id,
@Default(false) bool syncing,
String? parentId, String? parentId,
required String userId, required String userId,
String? path, String? path,
@ -72,7 +74,7 @@ class SyncedItem with _$SyncedItem {
_ => SyncStatus.partially, _ => SyncStatus.partially,
}; };
String? get taskId => null; String? get taskId => task?.taskId;
bool get childHasTask => false; bool get childHasTask => false;
@ -126,6 +128,7 @@ class SyncedItem with _$SyncedItem {
parentId: isarSyncedItem.parentId, parentId: isarSyncedItem.parentId,
userId: isarSyncedItem.userId ?? "", userId: isarSyncedItem.userId ?? "",
sortName: isarSyncedItem.sortName, sortName: isarSyncedItem.sortName,
syncing: isarSyncedItem.syncing,
path: joinAll([savePath, isarSyncedItem.path ?? ""]), path: joinAll([savePath, isarSyncedItem.path ?? ""]),
fileSize: isarSyncedItem.fileSize, fileSize: isarSyncedItem.fileSize,
videoFileName: isarSyncedItem.videoFileName, videoFileName: isarSyncedItem.videoFileName,
@ -155,21 +158,25 @@ class SyncedItem with _$SyncedItem {
enum SyncStatus { enum SyncStatus {
complete( complete(
"Synced",
Color.fromARGB(255, 141, 214, 58), Color.fromARGB(255, 141, 214, 58),
IconsaxOutline.tick_circle, IconsaxOutline.tick_circle,
), ),
partially( partially(
"Partially",
Color.fromARGB(255, 221, 135, 23), Color.fromARGB(255, 221, 135, 23),
IconsaxOutline.more_circle, IconsaxOutline.more_circle,
), ),
; ;
const SyncStatus(this.label, this.color, this.icon); const SyncStatus(this.color, this.icon);
final Color color; final Color color;
final String label; String label(BuildContext context) {
return switch (this) {
SyncStatus.partially => context.localized.syncStatusPartially,
SyncStatus.complete => context.localized.syncStatusSynced,
};
}
final IconData icon; final IconData icon;
} }
@ -183,14 +190,14 @@ extension StatusExtension on TaskStatus {
TaskStatus.paused => Colors.orangeAccent, TaskStatus.paused => Colors.orangeAccent,
}; };
String get name => switch (this) { String name(BuildContext context) => switch (this) {
TaskStatus.enqueued => 'Enqueued', TaskStatus.enqueued => context.localized.syncStatusEnqueued,
TaskStatus.running => 'Running', TaskStatus.running => context.localized.syncStatusRunning,
TaskStatus.complete => 'Complete', TaskStatus.complete => context.localized.syncStatusComplete,
TaskStatus.notFound => 'Not Found', TaskStatus.notFound => context.localized.syncStatusNotFound,
TaskStatus.failed => 'Failed', TaskStatus.failed => context.localized.syncStatusFailed,
TaskStatus.canceled => 'Canceled', TaskStatus.canceled => context.localized.syncStatusCanceled,
TaskStatus.waitingToRetry => 'Waiting To Retry', TaskStatus.waitingToRetry => context.localized.syncStatusWaitingToRetry,
TaskStatus.paused => 'Paused', TaskStatus.paused => context.localized.syncStatusPaused,
}; };
} }

View file

@ -17,6 +17,7 @@ final _privateConstructorUsedError = UnsupportedError(
/// @nodoc /// @nodoc
mixin _$SyncedItem { mixin _$SyncedItem {
String get id => throw _privateConstructorUsedError; String get id => throw _privateConstructorUsedError;
bool get syncing => throw _privateConstructorUsedError;
String? get parentId => throw _privateConstructorUsedError; String? get parentId => throw _privateConstructorUsedError;
String get userId => throw _privateConstructorUsedError; String get userId => throw _privateConstructorUsedError;
String? get path => throw _privateConstructorUsedError; String? get path => throw _privateConstructorUsedError;
@ -48,6 +49,7 @@ abstract class $SyncedItemCopyWith<$Res> {
@useResult @useResult
$Res call( $Res call(
{String id, {String id,
bool syncing,
String? parentId, String? parentId,
String userId, String userId,
String? path, String? path,
@ -82,6 +84,7 @@ class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem>
@override @override
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? syncing = null,
Object? parentId = freezed, Object? parentId = freezed,
Object? userId = null, Object? userId = null,
Object? path = freezed, Object? path = freezed,
@ -101,6 +104,10 @@ class _$SyncedItemCopyWithImpl<$Res, $Val extends SyncedItem>
? _value.id ? _value.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String, as String,
syncing: null == syncing
? _value.syncing
: syncing // ignore: cast_nullable_to_non_nullable
as bool,
parentId: freezed == parentId parentId: freezed == parentId
? _value.parentId ? _value.parentId
: parentId // ignore: cast_nullable_to_non_nullable : parentId // ignore: cast_nullable_to_non_nullable
@ -195,6 +202,7 @@ abstract class _$$SyncItemImplCopyWith<$Res>
@useResult @useResult
$Res call( $Res call(
{String id, {String id,
bool syncing,
String? parentId, String? parentId,
String userId, String userId,
String? path, String? path,
@ -229,6 +237,7 @@ class __$$SyncItemImplCopyWithImpl<$Res>
@override @override
$Res call({ $Res call({
Object? id = null, Object? id = null,
Object? syncing = null,
Object? parentId = freezed, Object? parentId = freezed,
Object? userId = null, Object? userId = null,
Object? path = freezed, Object? path = freezed,
@ -248,6 +257,10 @@ class __$$SyncItemImplCopyWithImpl<$Res>
? _value.id ? _value.id
: id // ignore: cast_nullable_to_non_nullable : id // ignore: cast_nullable_to_non_nullable
as String, as String,
syncing: null == syncing
? _value.syncing
: syncing // ignore: cast_nullable_to_non_nullable
as bool,
parentId: freezed == parentId parentId: freezed == parentId
? _value.parentId ? _value.parentId
: parentId // ignore: cast_nullable_to_non_nullable : parentId // ignore: cast_nullable_to_non_nullable
@ -309,6 +322,7 @@ class __$$SyncItemImplCopyWithImpl<$Res>
class _$SyncItemImpl extends _SyncItem { class _$SyncItemImpl extends _SyncItem {
_$SyncItemImpl( _$SyncItemImpl(
{required this.id, {required this.id,
this.syncing = false,
this.parentId, this.parentId,
required this.userId, required this.userId,
this.path, this.path,
@ -329,6 +343,9 @@ class _$SyncItemImpl extends _SyncItem {
@override @override
final String id; final String id;
@override @override
@JsonKey()
final bool syncing;
@override
final String? parentId; final String? parentId;
@override @override
final String userId; final String userId;
@ -373,7 +390,7 @@ class _$SyncItemImpl extends _SyncItem {
@override @override
String toString() { String toString() {
return 'SyncedItem(id: $id, parentId: $parentId, userId: $userId, path: $path, markedForDelete: $markedForDelete, sortName: $sortName, fileSize: $fileSize, videoFileName: $videoFileName, introOutSkipModel: $introOutSkipModel, 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, introOutSkipModel: $introOutSkipModel, fTrickPlayModel: $fTrickPlayModel, fImages: $fImages, fChapters: $fChapters, subtitles: $subtitles, userData: $userData)';
} }
@override @override
@ -382,6 +399,7 @@ class _$SyncItemImpl extends _SyncItem {
(other.runtimeType == runtimeType && (other.runtimeType == runtimeType &&
other is _$SyncItemImpl && other is _$SyncItemImpl &&
(identical(other.id, id) || other.id == id) && (identical(other.id, id) || other.id == id) &&
(identical(other.syncing, syncing) || other.syncing == syncing) &&
(identical(other.parentId, parentId) || (identical(other.parentId, parentId) ||
other.parentId == parentId) && other.parentId == parentId) &&
(identical(other.userId, userId) || other.userId == userId) && (identical(other.userId, userId) || other.userId == userId) &&
@ -411,6 +429,7 @@ class _$SyncItemImpl extends _SyncItem {
int get hashCode => Object.hash( int get hashCode => Object.hash(
runtimeType, runtimeType,
id, id,
syncing,
parentId, parentId,
userId, userId,
path, path,
@ -437,6 +456,7 @@ class _$SyncItemImpl extends _SyncItem {
abstract class _SyncItem extends SyncedItem { abstract class _SyncItem extends SyncedItem {
factory _SyncItem( factory _SyncItem(
{required final String id, {required final String id,
final bool syncing,
final String? parentId, final String? parentId,
required final String userId, required final String userId,
final String? path, final String? path,
@ -455,6 +475,8 @@ abstract class _SyncItem extends SyncedItem {
@override @override
String get id; String get id;
@override @override
bool get syncing;
@override
String? get parentId; String? get parentId;
@override @override
String get userId; String get userId;

View file

@ -13,6 +13,7 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:isar/isar.dart'; import 'package:isar/isar.dart';
import 'package:path/path.dart' as path; import 'package:path/path.dart' as path;
import 'package:path_provider/path_provider.dart';
import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart'; import 'package:fladder/jellyfin/jellyfin_open_api.swagger.dart';
import 'package:fladder/models/item_base_model.dart'; import 'package:fladder/models/item_base_model.dart';
@ -35,6 +36,7 @@ import 'package:fladder/providers/settings/client_settings_provider.dart';
import 'package:fladder/providers/sync/background_download_provider.dart'; import 'package:fladder/providers/sync/background_download_provider.dart';
import 'package:fladder/providers/user_provider.dart'; import 'package:fladder/providers/user_provider.dart';
import 'package:fladder/screens/shared/fladder_snackbar.dart'; import 'package:fladder/screens/shared/fladder_snackbar.dart';
import 'package:fladder/util/localization_helper.dart';
final syncProvider = StateNotifierProvider<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError()); final syncProvider = StateNotifierProvider<SyncNotifier, SyncSettingsModel>((ref) => throw UnimplementedError());
@ -46,6 +48,7 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
} }
void _init() { void _init() {
cleanupTemporaryFiles();
ref.listen( ref.listen(
userProvider, userProvider,
(previous, next) { (previous, next) {
@ -84,6 +87,35 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
}); });
} }
Future<void> cleanupTemporaryFiles() async {
// List of directories to check
final directories = [
//Desktop directory
await getTemporaryDirectory(),
//Mobile directory
await getApplicationSupportDirectory(),
];
for (final dir in directories) {
final List<FileSystemEntity> files = dir.listSync();
for (var file in files) {
if (file is File) {
final fileName = file.path.split(Platform.pathSeparator).last;
final fileSize = await file.length();
if (fileName.startsWith('com.bbflight.background_downloader') && fileSize != 0) {
try {
await file.delete();
log('Deleted temporary file: $fileName from ${dir.path}');
} catch (e) {
log('Failed to delete file $fileName: $e');
}
}
}
}
}
}
final Ref ref; final Ref ref;
final Isar? isar; final Isar? isar;
final Directory mobileDirectory; final Directory mobileDirectory;
@ -190,19 +222,20 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
return syncedItem.createItemModel(ref); return syncedItem.createItemModel(ref);
} }
Future<SyncedItem?> addSyncItem(BuildContext? context, ItemBaseModel item) async { Future<void> addSyncItem(BuildContext? context, ItemBaseModel item) async {
if (context == null) return null; if (context == null) return;
if (saveDirectory == null) { if (saveDirectory == null) {
String? selectedDirectory = await FilePicker.platform.getDirectoryPath(dialogTitle: 'Select downloads folder'); String? selectedDirectory =
await FilePicker.platform.getDirectoryPath(dialogTitle: context.localized.syncSelectDownloadsFolder);
if (selectedDirectory?.isEmpty == true) { if (selectedDirectory?.isEmpty == true) {
fladderSnackbar(context, title: "No sync folder setup"); fladderSnackbar(context, title: context.localized.syncNoFolderSetup);
return null; return;
} }
ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory); ref.read(clientSettingsProvider.notifier).setSyncPath(selectedDirectory);
} }
fladderSnackbar(context, title: "Added ${item.detailedName(context)} for syncing"); 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),
SeriesModel series => await syncSeries(series), SeriesModel series => await syncSeries(series),
@ -211,12 +244,12 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
}; };
fladderSnackbar(context, fladderSnackbar(context,
title: newSync != null title: newSync != null
? "Started syncing ${item.detailedName(context)}" ? context.localized.startedSyncingItem(item.detailedName(context) ?? "Unknown")
: "Unable to sync ${item.detailedName(context)}, type not supported?"); : context.localized.unableToSyncItem(item.detailedName(context) ?? "Unknown"));
return newSync; return;
} }
Future<bool> removeSync(SyncedItem? item) async { Future<bool> removeSync(BuildContext context, SyncedItem? item) async {
try { try {
if (item == null) return false; if (item == null) return false;
@ -259,6 +292,8 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
} catch (e) { } catch (e) {
log('Error deleting synced item'); log('Error deleting synced item');
log(e.toString()); log(e.toString());
state = state.copyWith(items: state.items.map((e) => e.copyWith(markedForDelete: false)).toList());
fladderSnackbar(context, title: context.localized.syncRemoveUnableToDeleteItem);
return false; return false;
} }
} }
@ -366,14 +401,23 @@ class SyncNotifier extends StateNotifier<SyncSettingsModel> {
isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? ""))); isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncedItem, syncPath ?? "")));
} }
Future<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem) async { Future<SyncedItem> deleteFullSyncFiles(SyncedItem syncedItem, DownloadTask? task) async {
await syncedItem.deleteDatFiles(ref); await syncedItem.deleteDatFiles(ref);
ref.read(downloadTasksProvider(syncedItem.id).notifier).update((state) => DownloadStream.empty()); ref.read(downloadTasksProvider(syncedItem.id).notifier).update((state) => DownloadStream.empty());
final taskId = task?.taskId;
if (taskId != null) {
ref.read(backgroundDownloaderProvider).cancelTaskWithId(taskId);
}
cleanupTemporaryFiles();
refresh(); refresh();
return syncedItem; return syncedItem;
} }
Future<DownloadStream?> syncVideoFile(SyncedItem syncItem, bool skipDownload) async { Future<DownloadStream?> syncVideoFile(SyncedItem syncItem, bool skipDownload) async {
cleanupTemporaryFiles();
final playbackResponse = await api.itemsItemIdPlaybackInfoPost( final playbackResponse = await api.itemsItemIdPlaybackInfoPost(
itemId: syncItem.id, itemId: syncItem.id,
body: const PlaybackInfoDto( body: const PlaybackInfoDto(
@ -480,27 +524,37 @@ extension SyncNotifierHelpers on SyncNotifier {
final Directory? parentDirectory = parent?.directory; final Directory? parentDirectory = parent?.directory;
SyncedItem syncItem = SyncedItem(id: item.id, userId: ref.read(userProvider)?.id ?? "");
final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id])); final directory = Directory(path.joinAll([(parentDirectory ?? saveDirectory)?.path ?? "", item.id]));
await directory.create(recursive: true); await directory.create(recursive: true);
File dataFile = File(path.joinAll([directory.path, 'data.json'])); File dataFile = File(path.joinAll([directory.path, 'data.json']));
await dataFile.writeAsString(jsonEncode(response.toJson())); await dataFile.writeAsString(jsonEncode(response.toJson()));
final imageData = await saveImageData(item.images, directory); final imageData = await saveImageData(item.images, directory);
final origChapters = Chapter.chaptersFromInfo(item.id, response.chapters ?? [], ref);
return syncItem.copyWith( SyncedItem syncItem = SyncedItem(
syncing: true,
id: item.id, id: item.id,
parentId: parent?.id, parentId: parent?.id,
sortName: response.sortName, sortName: response.sortName,
fImages: imageData,
userId: ref.read(userProvider)?.id ?? "",
path: directory.path, 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) ?? [], fChapters: await saveChapterImages(origChapters, directory) ?? [],
fileSize: response.mediaSources?.firstOrNull?.size ?? 0, fileSize: response.mediaSources?.firstOrNull?.size ?? 0,
fImages: imageData, syncing: false,
videoFileName: response.path?.split('/').lastOrNull ?? "", videoFileName: response.path?.split('/').lastOrNull ?? "",
userData: item.userData,
); );
} }
@ -528,7 +582,7 @@ extension SyncNotifierHelpers on SyncNotifier {
await syncVideoFile(syncItem, skipDownload); await syncVideoFile(syncItem, skipDownload);
await isar?.writeAsync((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath))); isar?.write((isar) => syncedItems?.put(ISyncedItem.fromSynced(syncItem, syncPath)));
return syncItem; return syncItem;
} }

View file

@ -15,8 +15,8 @@ import 'package:fladder/screens/shared/default_alert_dialog.dart';
import 'package:fladder/screens/shared/media/poster_widget.dart'; import 'package:fladder/screens/shared/media/poster_widget.dart';
import 'package:fladder/screens/syncing/sync_child_item.dart'; import 'package:fladder/screens/syncing/sync_child_item.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/screens/syncing/widgets/sync_markedfordelete.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart'; import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart';
import 'package:fladder/util/adaptive_layout.dart'; import 'package:fladder/util/adaptive_layout.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';
@ -55,7 +55,7 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
final syncChildren = ref.read(syncProvider.notifier).getChildren(syncedItem); final syncChildren = ref.read(syncProvider.notifier).getChildren(syncedItem);
final downloadTask = ref.read(downloadTasksProvider(syncedItem.id)); final downloadTask = ref.read(downloadTasksProvider(syncedItem.id));
return SyncMarkedForDelete( return SyncStatusOverlay(
syncedItem: syncedItem, syncedItem: syncedItem,
child: ActionContent( child: ActionContent(
title: Row( title: Row(
@ -135,7 +135,9 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
icon: const Icon(IconsaxBold.play), icon: const Icon(IconsaxBold.play),
), ),
IconButton( IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem), onPressed: () => ref
.read(syncProvider.notifier)
.deleteFullSyncFiles(syncedItem, combinedStream?.task),
icon: const Icon(IconsaxBold.stop), icon: const Icon(IconsaxBold.stop),
), ),
], ],
@ -177,7 +179,7 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
context.localized.syncRemoveDataTitle, context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataDesc, context.localized.syncRemoveDataDesc,
(context) { (context) {
ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem); ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
Navigator.of(context).pop(); Navigator.of(context).pop();
}, },
context.localized.delete, context.localized.delete,
@ -217,7 +219,7 @@ class _SyncItemDetailsState extends ConsumerState<SyncItemDetails> {
context.localized.syncDeleteItemTitle, context.localized.syncDeleteItemTitle,
context.localized.syncDeleteItemDesc(baseItem?.detailedName(context) ?? ""), context.localized.syncDeleteItemDesc(baseItem?.detailedName(context) ?? ""),
(context) async { (context) async {
await ref.read(syncProvider.notifier).removeSync(syncedItem); await ref.read(syncProvider.notifier).removeSync(context, syncedItem);
Navigator.pop(context); Navigator.pop(context);
Navigator.pop(context); Navigator.pop(context);
}, },

View file

@ -1,18 +1,20 @@
import 'package:flutter/material.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.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/syncing/sync_item_details.dart'; import 'package:fladder/screens/syncing/sync_item_details.dart';
import 'package:fladder/screens/syncing/sync_widgets.dart'; import 'package:fladder/screens/syncing/sync_widgets.dart';
import 'package:fladder/screens/syncing/widgets/sync_markedfordelete.dart';
import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart'; import 'package:fladder/screens/syncing/widgets/sync_progress_builder.dart';
import 'package:fladder/screens/syncing/widgets/sync_status_overlay.dart';
import 'package:fladder/util/fladder_image.dart'; import 'package:fladder/util/fladder_image.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart'; import 'package:fladder/util/size_formatting.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncListItem extends ConsumerStatefulWidget { class SyncListItem extends ConsumerStatefulWidget {
final SyncedItem syncedItem; final SyncedItem syncedItem;
@ -29,7 +31,7 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem); final baseItem = ref.read(syncProvider.notifier).getItem(syncedItem);
return Padding( return Padding(
padding: const EdgeInsets.symmetric(vertical: 8), padding: const EdgeInsets.symmetric(vertical: 8),
child: SyncMarkedForDelete( child: SyncStatusOverlay(
syncedItem: syncedItem, syncedItem: syncedItem,
child: Card( child: Card(
elevation: 1, elevation: 1,
@ -53,7 +55,7 @@ class SyncListItemState extends ConsumerState<SyncListItem> {
context.localized.deleteItem(baseItem?.detailedName(context) ?? ""), context.localized.deleteItem(baseItem?.detailedName(context) ?? ""),
context.localized.syncDeletePopupPermanent, context.localized.syncDeletePopupPermanent,
(context) async { (context) async {
ref.read(syncProvider.notifier).removeSync(syncedItem); ref.read(syncProvider.notifier).removeSync(context, syncedItem);
Navigator.of(context).pop(); Navigator.of(context).pop();
return true; return true;
}, },

View file

@ -1,5 +1,9 @@
import 'package:flutter/material.dart';
import 'package:background_downloader/background_downloader.dart'; import 'package:background_downloader/background_downloader.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/items/season_model.dart'; import 'package:fladder/models/items/season_model.dart';
import 'package:fladder/models/items/series_model.dart'; import 'package:fladder/models/items/series_model.dart';
@ -10,8 +14,6 @@ import 'package:fladder/providers/sync/sync_provider_helpers.dart';
import 'package:fladder/providers/sync_provider.dart'; import 'package:fladder/providers/sync_provider.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncLabel extends ConsumerWidget { class SyncLabel extends ConsumerWidget {
final String? label; final String? label;
@ -28,7 +30,7 @@ class SyncLabel extends ConsumerWidget {
child: Padding( child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2), padding: const EdgeInsets.symmetric(horizontal: 6, vertical: 2),
child: Text( child: Text(
label ?? status.label, label ?? status.label(context),
style: Theme.of(context).textTheme.bodyMedium?.copyWith( style: Theme.of(context).textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.bold, fontWeight: FontWeight.bold,
color: status.color, color: status.color,
@ -55,7 +57,7 @@ class SyncProgressBar extends ConsumerWidget {
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Text(downloadStatus.name), Text(downloadStatus.name(context)),
Row( Row(
crossAxisAlignment: CrossAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center,
mainAxisSize: MainAxisSize.min, mainAxisSize: MainAxisSize.min,
@ -82,7 +84,7 @@ class SyncProgressBar extends ConsumerWidget {
icon: const Icon(IconsaxBold.play), icon: const Icon(IconsaxBold.play),
), ),
IconButton( IconButton(
onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item), onPressed: () => ref.read(syncProvider.notifier).deleteFullSyncFiles(item, downloadTask),
icon: const Icon(IconsaxBold.stop), icon: const Icon(IconsaxBold.stop),
) )
], ],
@ -129,7 +131,7 @@ class SyncSubtitle extends ConsumerWidget {
); );
}, },
), ),
_ => Text(syncStatus.label), _ => Text(syncStatus.label(context)),
}, },
), ),
), ),

View file

@ -5,12 +5,12 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/util/list_padding.dart'; import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart';
///This is a wrapper widget for marking a synced item as deleted (while it is being deleted) class SyncStatusOverlay extends ConsumerWidget {
class SyncMarkedForDelete extends ConsumerWidget {
final SyncedItem syncedItem; final SyncedItem syncedItem;
final Widget child; final Widget child;
const SyncMarkedForDelete({required this.syncedItem, required this.child, super.key}); const SyncStatusOverlay({required this.syncedItem, required this.child, super.key});
@override @override
Widget build(BuildContext context, WidgetRef ref) { Widget build(BuildContext context, WidgetRef ref) {
@ -32,12 +32,35 @@ class SyncMarkedForDelete extends ConsumerWidget {
strokeCap: StrokeCap.round, strokeCap: StrokeCap.round,
valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.error), valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.error),
), ),
const Text("Deleting"), Text(context.localized.syncOverlayDeleting),
const Icon(IconsaxOutline.trash) const Icon(IconsaxOutline.trash)
].addPadding(const EdgeInsets.symmetric(horizontal: 16)), ].addPadding(const EdgeInsets.symmetric(horizontal: 16)),
), ),
), ),
) ),
if (syncedItem.syncing)
Positioned.fill(
child: IgnorePointer(
child: Card(
elevation: 0,
semanticContainer: false,
color: Colors.black.withOpacity(0.6),
child: Row(
mainAxisSize: MainAxisSize.max,
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
CircularProgressIndicator.adaptive(
strokeCap: StrokeCap.round,
valueColor: AlwaysStoppedAnimation(Theme.of(context).colorScheme.error),
),
Text(context.localized.syncOverlaySyncing),
const Icon(IconsaxOutline.cloud_notif)
].addPadding(const EdgeInsets.symmetric(horizontal: 16)),
),
),
),
),
], ],
); );
} }

View file

@ -1,4 +1,8 @@
import 'package:flutter/material.dart';
import 'package:ficonsax/ficonsax.dart'; import 'package:ficonsax/ficonsax.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:fladder/models/items/episode_model.dart'; import 'package:fladder/models/items/episode_model.dart';
import 'package:fladder/models/syncing/sync_item.dart'; import 'package:fladder/models/syncing/sync_item.dart';
import 'package:fladder/providers/sync/sync_provider_helpers.dart'; import 'package:fladder/providers/sync/sync_provider_helpers.dart';
@ -10,8 +14,6 @@ import 'package:fladder/util/list_padding.dart';
import 'package:fladder/util/localization_helper.dart'; import 'package:fladder/util/localization_helper.dart';
import 'package:fladder/util/size_formatting.dart'; import 'package:fladder/util/size_formatting.dart';
import 'package:fladder/widgets/shared/icon_button_await.dart'; import 'package:fladder/widgets/shared/icon_button_await.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
class SyncedEpisodeItem extends ConsumerStatefulWidget { class SyncedEpisodeItem extends ConsumerStatefulWidget {
const SyncedEpisodeItem({ const SyncedEpisodeItem({
@ -106,7 +108,7 @@ class _SyncedEpisodeItemState extends ConsumerState<SyncedEpisodeItem> {
context.localized.syncRemoveDataTitle, context.localized.syncRemoveDataTitle,
context.localized.syncRemoveDataDesc, context.localized.syncRemoveDataDesc,
(context) async { (context) async {
await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem); await ref.read(syncProvider.notifier).deleteFullSyncFiles(syncedItem, downloadTask.task);
Navigator.pop(context); Navigator.pop(context);
}, },
context.localized.delete, context.localized.delete,

View file

@ -7,7 +7,7 @@ project(runner LANGUAGES CXX)
set(BINARY_NAME "Fladder") set(BINARY_NAME "Fladder")
# The unique GTK application identifier for this application. See: # The unique GTK application identifier for this application. See:
# https://wiki.gnome.org/HowDoI/ChooseApplicationID # https://wiki.gnome.org/HowDoI/ChooseApplicationID
set(APPLICATION_ID "com.example.fladder") set(APPLICATION_ID "nl.jknaapen.fladder")
# Explicitly opt in to modern CMake behaviors to avoid warnings with recent # Explicitly opt in to modern CMake behaviors to avoid warnings with recent
# versions of CMake. # versions of CMake.

View file

@ -11,4 +11,4 @@ PRODUCT_NAME = Fladder
PRODUCT_BUNDLE_IDENTIFIER = nl.jknaapen.fladder PRODUCT_BUNDLE_IDENTIFIER = nl.jknaapen.fladder
// The copyright displayed in application information // The copyright displayed in application information
PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. PRODUCT_COPYRIGHT = Copyright © 2023 Donutware. All rights reserved.

View file

@ -89,11 +89,11 @@ BEGIN
BEGIN BEGIN
BLOCK "040904e4" BLOCK "040904e4"
BEGIN BEGIN
VALUE "CompanyName", "com.example" "\0" VALUE "CompanyName", "DonutWare" "\0"
VALUE "FileDescription", "fladder" "\0" VALUE "FileDescription", "fladder" "\0"
VALUE "FileVersion", VERSION_AS_STRING "\0" VALUE "FileVersion", VERSION_AS_STRING "\0"
VALUE "InternalName", "nl.jknaapen.fladder" "\0" VALUE "InternalName", "nl.jknaapen.fladder" "\0"
VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" VALUE "LegalCopyright", "Copyright (C) 2023 DonutWare. All rights reserved." "\0"
VALUE "OriginalFilename", "fladder.exe" "\0" VALUE "OriginalFilename", "fladder.exe" "\0"
VALUE "ProductName", "fladder" "\0" VALUE "ProductName", "fladder" "\0"
VALUE "ProductVersion", VERSION_AS_STRING "\0" VALUE "ProductVersion", VERSION_AS_STRING "\0"