From 7b00d0f7e45820083d6c435c622d90f5084b65ac Mon Sep 17 00:00:00 2001 From: Schnitzel5 Date: Thu, 13 Feb 2025 23:41:26 +0100 Subject: [PATCH] finished snapshots and added auto sync --- lib/l10n/app_de.arb | 20 +- lib/l10n/app_en.arb | 18 ++ lib/models/sync_preference.dart | 7 +- lib/models/sync_preference.g.dart | 149 +++++++++-- lib/modules/main_view/main_screen.dart | 18 ++ .../data_and_storage/providers/restore.dart | 2 +- .../data_and_storage/providers/restore.g.dart | 2 +- .../sync/providers/sync_providers.dart | 21 +- .../sync/providers/sync_providers.g.dart | 2 +- lib/modules/more/settings/sync/sync.dart | 250 ++++++++++++++++-- lib/services/fetch_sources_list.dart | 6 +- lib/services/sync_server.dart | 56 +++- lib/services/sync_server.g.dart | 2 +- 13 files changed, 492 insertions(+), 61 deletions(-) diff --git a/lib/l10n/app_de.arb b/lib/l10n/app_de.arb index 010fb536..ba4c611e 100644 --- a/lib/l10n/app_de.arb +++ b/lib/l10n/app_de.arb @@ -225,8 +225,26 @@ "sync_pending_track": "Ausstehende Änderungen für Trackings", "sync_snapshot_creating": "Erstelle Snapshot...", "sync_snapshot_created": "Snapshot wurde erstellt!", + "sync_snapshot_deleting": "Lösche Snapshot...", + "sync_snapshot_deleted": "Snapshot wurde gelöscht!", "sync_snapshot_no_data": "Keine Daten zum Sichern! Lade erstmal alles hoch!", - "server_error": "Server error!", + "sync_browse_snapshots": "Durchsuche ältere Backups", + "sync_snapshots": "Snapshots", + "sync_load_snapshot": "Snapshot laden", + "sync_delete_snapshot": "Snapshot löschen", + "sync_auto": "Auto Sync", + "sync_auto_warning": "Auto Sync ist derzeit ein experimentelles Feature!", + "sync_auto_off": "Aus", + "sync_auto_30_seconds": "Alle 30 Sekunden", + "sync_auto_1_minute": "Jede Minute", + "sync_auto_5_minutes": "Alle 5 Minuten", + "sync_auto_10_minutes": "Alle 10 Minuten", + "sync_auto_30_minutes": "Alle 30 Minuten", + "sync_auto_1_hour": "Jede Stunde", + "sync_auto_3_hours": "Alle 3 Stunden", + "sync_auto_6_hours": "Alle 6 Stunden", + "sync_auto_12_hours": "Alle 12 Stunden", + "server_error": "Server Fehler!", "dialog_confirm": "Fortfahren", "description": "Beschreibung", "full_screen_player": "Vollbildmodus aktivieren", diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index 8204e7a0..fa1d641e 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -225,7 +225,25 @@ "sync_pending_track": "Track changes pending", "sync_snapshot_creating": "Creating snapshot...", "sync_snapshot_created": "Snapshot created!", + "sync_snapshot_deleting": "Deleting snapshot...", + "sync_snapshot_deleted": "Snapshot deleted!", "sync_snapshot_no_data": "No data to create a snapshot! Do a full upload first!", + "sync_browse_snapshots": "Browse older backups", + "sync_snapshots": "Snapshots", + "sync_load_snapshot": "Load snapshot", + "sync_delete_snapshot": "Delete snapshot", + "sync_auto": "Auto Sync", + "sync_auto_warning": "Auto Sync is currently an experimental feature!", + "sync_auto_off": "Off", + "sync_auto_30_seconds": "Every 30 seconds", + "sync_auto_1_minute": "Every 1 minute", + "sync_auto_5_minutes": "Every 5 minutes", + "sync_auto_10_minutes": "Every 10 minutes", + "sync_auto_30_minutes": "Every 30 minutes", + "sync_auto_1_hour": "Every 1 hour", + "sync_auto_3_hours": "Every 3 hours", + "sync_auto_6_hours": "Every 6 hours", + "sync_auto_12_hours": "Every 12 hours", "server_error": "Server error!", "dialog_confirm": "Confirm", "description": "Description", diff --git a/lib/models/sync_preference.dart b/lib/models/sync_preference.dart index a37619af..cb841d1a 100644 --- a/lib/models/sync_preference.dart +++ b/lib/models/sync_preference.dart @@ -20,6 +20,8 @@ class SyncPreference { bool syncOn = false; + int autoSyncFrequency = 0; + SyncPreference({ this.syncId, this.email, @@ -29,6 +31,7 @@ class SyncPreference { this.lastDownload, this.server, this.syncOn = false, + this.autoSyncFrequency = 0, }); SyncPreference.fromJson(Map json) { @@ -40,6 +43,7 @@ class SyncPreference { lastDownload = json['lastDownload']; server = json['server']; syncOn = json['syncOn'] ?? false; + syncOn = json['autoSyncFrequency'] ?? 0; } Map toJson() => { @@ -49,6 +53,7 @@ class SyncPreference { 'lastSync': lastSync, 'lastUpload': lastUpload, 'lastDownload': lastDownload, - 'syncOn': syncOn + 'syncOn': syncOn, + 'autoSyncFrequency': autoSyncFrequency, }; } diff --git a/lib/models/sync_preference.g.dart b/lib/models/sync_preference.g.dart index 739c2dae..4f7dbaed 100644 --- a/lib/models/sync_preference.g.dart +++ b/lib/models/sync_preference.g.dart @@ -22,33 +22,38 @@ const SyncPreferenceSchema = CollectionSchema( name: r'authToken', type: IsarType.string, ), - r'email': PropertySchema( + r'autoSyncFrequency': PropertySchema( id: 1, + name: r'autoSyncFrequency', + type: IsarType.long, + ), + r'email': PropertySchema( + id: 2, name: r'email', type: IsarType.string, ), r'lastDownload': PropertySchema( - id: 2, + id: 3, name: r'lastDownload', type: IsarType.long, ), r'lastSync': PropertySchema( - id: 3, + id: 4, name: r'lastSync', type: IsarType.long, ), r'lastUpload': PropertySchema( - id: 4, + id: 5, name: r'lastUpload', type: IsarType.long, ), r'server': PropertySchema( - id: 5, + id: 6, name: r'server', type: IsarType.string, ), r'syncOn': PropertySchema( - id: 6, + id: 7, name: r'syncOn', type: IsarType.bool, ) @@ -101,12 +106,13 @@ void _syncPreferenceSerialize( Map> allOffsets, ) { writer.writeString(offsets[0], object.authToken); - writer.writeString(offsets[1], object.email); - writer.writeLong(offsets[2], object.lastDownload); - writer.writeLong(offsets[3], object.lastSync); - writer.writeLong(offsets[4], object.lastUpload); - writer.writeString(offsets[5], object.server); - writer.writeBool(offsets[6], object.syncOn); + writer.writeLong(offsets[1], object.autoSyncFrequency); + writer.writeString(offsets[2], object.email); + writer.writeLong(offsets[3], object.lastDownload); + writer.writeLong(offsets[4], object.lastSync); + writer.writeLong(offsets[5], object.lastUpload); + writer.writeString(offsets[6], object.server); + writer.writeBool(offsets[7], object.syncOn); } SyncPreference _syncPreferenceDeserialize( @@ -117,13 +123,14 @@ SyncPreference _syncPreferenceDeserialize( ) { final object = SyncPreference( authToken: reader.readStringOrNull(offsets[0]), - email: reader.readStringOrNull(offsets[1]), - lastDownload: reader.readLongOrNull(offsets[2]), - lastSync: reader.readLongOrNull(offsets[3]), - lastUpload: reader.readLongOrNull(offsets[4]), - server: reader.readStringOrNull(offsets[5]), + autoSyncFrequency: reader.readLongOrNull(offsets[1]) ?? 0, + email: reader.readStringOrNull(offsets[2]), + lastDownload: reader.readLongOrNull(offsets[3]), + lastSync: reader.readLongOrNull(offsets[4]), + lastUpload: reader.readLongOrNull(offsets[5]), + server: reader.readStringOrNull(offsets[6]), syncId: id, - syncOn: reader.readBoolOrNull(offsets[6]) ?? false, + syncOn: reader.readBoolOrNull(offsets[7]) ?? false, ); return object; } @@ -138,16 +145,18 @@ P _syncPreferenceDeserializeProp

( case 0: return (reader.readStringOrNull(offset)) as P; case 1: - return (reader.readStringOrNull(offset)) as P; + return (reader.readLongOrNull(offset) ?? 0) as P; case 2: - return (reader.readLongOrNull(offset)) as P; + return (reader.readStringOrNull(offset)) as P; case 3: return (reader.readLongOrNull(offset)) as P; case 4: return (reader.readLongOrNull(offset)) as P; case 5: - return (reader.readStringOrNull(offset)) as P; + return (reader.readLongOrNull(offset)) as P; case 6: + return (reader.readStringOrNull(offset)) as P; + case 7: return (reader.readBoolOrNull(offset) ?? false) as P; default: throw IsarError('Unknown property with id $propertyId'); @@ -402,6 +411,62 @@ extension SyncPreferenceQueryFilter }); } + QueryBuilder + autoSyncFrequencyEqualTo(int value) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.equalTo( + property: r'autoSyncFrequency', + value: value, + )); + }); + } + + QueryBuilder + autoSyncFrequencyGreaterThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.greaterThan( + include: include, + property: r'autoSyncFrequency', + value: value, + )); + }); + } + + QueryBuilder + autoSyncFrequencyLessThan( + int value, { + bool include = false, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.lessThan( + include: include, + property: r'autoSyncFrequency', + value: value, + )); + }); + } + + QueryBuilder + autoSyncFrequencyBetween( + int lower, + int upper, { + bool includeLower = true, + bool includeUpper = true, + }) { + return QueryBuilder.apply(this, (query) { + return query.addFilterCondition(FilterCondition.between( + property: r'autoSyncFrequency', + lower: lower, + includeLower: includeLower, + upper: upper, + includeUpper: includeUpper, + )); + }); + } + QueryBuilder emailIsNull() { return QueryBuilder.apply(this, (query) { @@ -1038,6 +1103,20 @@ extension SyncPreferenceQuerySortBy }); } + QueryBuilder + sortByAutoSyncFrequency() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'autoSyncFrequency', Sort.asc); + }); + } + + QueryBuilder + sortByAutoSyncFrequencyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'autoSyncFrequency', Sort.desc); + }); + } + QueryBuilder sortByEmail() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'email', Sort.asc); @@ -1133,6 +1212,20 @@ extension SyncPreferenceQuerySortThenBy }); } + QueryBuilder + thenByAutoSyncFrequency() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'autoSyncFrequency', Sort.asc); + }); + } + + QueryBuilder + thenByAutoSyncFrequencyDesc() { + return QueryBuilder.apply(this, (query) { + return query.addSortBy(r'autoSyncFrequency', Sort.desc); + }); + } + QueryBuilder thenByEmail() { return QueryBuilder.apply(this, (query) { return query.addSortBy(r'email', Sort.asc); @@ -1235,6 +1328,13 @@ extension SyncPreferenceQueryWhereDistinct }); } + QueryBuilder + distinctByAutoSyncFrequency() { + return QueryBuilder.apply(this, (query) { + return query.addDistinctBy(r'autoSyncFrequency'); + }); + } + QueryBuilder distinctByEmail( {bool caseSensitive = true}) { return QueryBuilder.apply(this, (query) { @@ -1290,6 +1390,13 @@ extension SyncPreferenceQueryProperty }); } + QueryBuilder + autoSyncFrequencyProperty() { + return QueryBuilder.apply(this, (query) { + return query.addPropertyName(r'autoSyncFrequency'); + }); + } + QueryBuilder emailProperty() { return QueryBuilder.apply(this, (query) { return query.addPropertyName(r'email'); diff --git a/lib/modules/main_view/main_screen.dart b/lib/modules/main_view/main_screen.dart index 5a33df84..565650d3 100644 --- a/lib/modules/main_view/main_screen.dart +++ b/lib/modules/main_view/main_screen.dart @@ -5,12 +5,14 @@ import 'package:flutter_riverpod/flutter_riverpod.dart'; import 'package:go_router/go_router.dart'; import 'package:google_fonts/google_fonts.dart'; import 'package:isar/isar.dart'; +import 'package:mangayomi/eval/model/m_bridge.dart'; import 'package:mangayomi/main.dart'; import 'package:mangayomi/models/chapter.dart'; import 'package:mangayomi/models/manga.dart'; import 'package:mangayomi/models/update.dart'; import 'package:mangayomi/models/source.dart'; import 'package:mangayomi/modules/more/settings/reader/providers/reader_state_provider.dart'; +import 'package:mangayomi/modules/more/settings/sync/providers/sync_providers.dart'; import 'package:mangayomi/modules/widgets/loading_icon.dart'; import 'package:mangayomi/services/fetch_anime_sources.dart'; import 'package:mangayomi/services/fetch_manga_sources.dart'; @@ -21,6 +23,7 @@ import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/router/router.dart'; import 'package:mangayomi/services/fetch_novel_sources.dart'; import 'package:mangayomi/services/fetch_sources_list.dart'; +import 'package:mangayomi/services/sync_server.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; import 'package:mangayomi/modules/library/providers/library_state_provider.dart'; import 'package:mangayomi/modules/more/providers/incognito_mode_state_provider.dart'; @@ -52,6 +55,8 @@ class _MainScreenState extends ConsumerState { } late final navigationOrder = ref.watch(navigationOrderStateProvider); + late final autoSyncFrequency = + ref.watch(synchingProvider(syncId: 1)).autoSyncFrequency; late String? location = ref.watch(routerCurrentLocationStateProvider(context)); late String defaultLocation = navigationOrder.first; @@ -63,6 +68,19 @@ class _MainScreenState extends ConsumerState { Timer.periodic(Duration(minutes: 5), (timer) { ref.read(checkAndBackupProvider); }); + if (autoSyncFrequency != 0) { + final l10n = l10nLocalizations(context)!; + Timer.periodic(Duration(seconds: autoSyncFrequency), (timer) { + try { + ref.read(syncServerProvider(syncId: 1).notifier).startSync(l10n); + } catch (e) { + botToast( + "Failed to sync! Maybe the sync server is down. Restart the app to resume auto sync."); + timer.cancel(); + } + }); + } + ref.watch(checkForUpdateProvider(context: context)); ref.watch(fetchMangaSourcesListProvider(id: null, reFresh: false)); ref.watch(fetchAnimeSourcesListProvider(id: null, reFresh: false)); diff --git a/lib/modules/more/data_and_storage/providers/restore.dart b/lib/modules/more/data_and_storage/providers/restore.dart index dfbc0b2b..6e4a89cd 100644 --- a/lib/modules/more/data_and_storage/providers/restore.dart +++ b/lib/modules/more/data_and_storage/providers/restore.dart @@ -186,7 +186,7 @@ void restoreBackup(Ref ref, Map backup, {bool full = true}) { } } if (full) { - ref.read(synchingProvider(syncId: 1).notifier).clearAllChangedParts(); + ref.read(synchingProvider(syncId: 1).notifier).clearAllChangedParts(false); ref.invalidate(themeModeStateProvider); ref.invalidate(blendLevelStateProvider); ref.invalidate(flexSchemeColorStateProvider); diff --git a/lib/modules/more/data_and_storage/providers/restore.g.dart b/lib/modules/more/data_and_storage/providers/restore.g.dart index 804eda90..dd003b73 100644 --- a/lib/modules/more/data_and_storage/providers/restore.g.dart +++ b/lib/modules/more/data_and_storage/providers/restore.g.dart @@ -173,7 +173,7 @@ class _DoRestoreProviderElement extends AutoDisposeProviderElement BuildContext get context => (origin as DoRestoreProvider).context; } -String _$restoreBackupHash() => r'4285c4f788c892ddcedb48951284e181cc821541'; +String _$restoreBackupHash() => r'0b6bdb8eff801da7efa7b3776f80e50bee4d4ad1'; /// See also [restoreBackup]. @ProviderFor(restoreBackup) diff --git a/lib/modules/more/settings/sync/providers/sync_providers.dart b/lib/modules/more/settings/sync/providers/sync_providers.dart index 6e25c5b4..959996ef 100644 --- a/lib/modules/more/settings/sync/providers/sync_providers.dart +++ b/lib/modules/more/settings/sync/providers/sync_providers.dart @@ -19,7 +19,7 @@ class Synching extends _$Synching { isar.writeTxnSync(() { isar.syncPreferences.putSync(syncPreference); }); - ref.invalidate(synchingProvider(syncId: syncId)); + ref.invalidateSelf(); ref.invalidate(syncServerProvider(syncId: syncId!)); } @@ -27,7 +27,7 @@ class Synching extends _$Synching { isar.writeTxnSync(() { isar.syncPreferences.putSync(state..authToken = null); }); - ref.invalidate(synchingProvider(syncId: syncId)); + ref.invalidateSelf(); ref.invalidate(syncServerProvider(syncId: syncId!)); } @@ -61,6 +61,13 @@ class Synching extends _$Synching { }); } + void setAutoSyncFrequency(int value) { + isar.writeTxnSync(() { + isar.syncPreferences.putSync(state..autoSyncFrequency = value); + }); + ref.invalidateSelf(); + } + List getAllChangedParts() { return isar.changedParts.filter().idIsNotNull().findAllSync(); } @@ -170,9 +177,13 @@ class Synching extends _$Synching { }); } - void clearAllChangedParts() { - isar.writeTxnSync(() { + void clearAllChangedParts(bool txn) { + if (txn) { + isar.writeTxnSync(() { + isar.changedParts.clearSync(); + }); + } else { isar.changedParts.clearSync(); - }); + } } } diff --git a/lib/modules/more/settings/sync/providers/sync_providers.g.dart b/lib/modules/more/settings/sync/providers/sync_providers.g.dart index e3141ea3..22520136 100644 --- a/lib/modules/more/settings/sync/providers/sync_providers.g.dart +++ b/lib/modules/more/settings/sync/providers/sync_providers.g.dart @@ -6,7 +6,7 @@ part of 'sync_providers.dart'; // RiverpodGenerator // ************************************************************************** -String _$synchingHash() => r'abef3be5c474e8bf6f9d2b5af7587caf061d1fbd'; +String _$synchingHash() => r'8a4f7f408bf0ac26f4a21368620051ecba3adf53'; /// Copied from Dart SDK class _SystemHash { diff --git a/lib/modules/more/settings/sync/sync.dart b/lib/modules/more/settings/sync/sync.dart index 963832ce..0c5047fc 100644 --- a/lib/modules/more/settings/sync/sync.dart +++ b/lib/modules/more/settings/sync/sync.dart @@ -10,17 +10,29 @@ import 'package:mangayomi/modules/more/settings/sync/widgets/sync_listile.dart'; import 'package:mangayomi/providers/l10n_providers.dart'; import 'package:mangayomi/services/sync_server.dart'; import 'package:mangayomi/utils/extensions/build_context_extensions.dart'; -import 'package:mangayomi/utils/language.dart'; class SyncScreen extends ConsumerWidget { const SyncScreen({super.key}); @override Widget build(BuildContext context, WidgetRef ref) { - final autoSyncOptions = [0, 30, 60, 300, 600, 1800, 3600]; final syncProvider = ref.watch(synchingProvider(syncId: 1)); final changedParts = ref.watch(synchingProvider(syncId: 1).notifier); + final autoSyncFrequency = + ref.watch(synchingProvider(syncId: 1)).autoSyncFrequency; final l10n = l10nLocalizations(context)!; + final autoSyncOptions = { + l10n.sync_auto_off: 0, + l10n.sync_auto_30_seconds: 30, + l10n.sync_auto_1_minute: 60, + l10n.sync_auto_5_minutes: 300, + l10n.sync_auto_10_minutes: 600, + l10n.sync_auto_30_minutes: 1800, + l10n.sync_auto_1_hour: 3600, + l10n.sync_auto_3_hours: 10800, + l10n.sync_auto_6_hours: 21600, + l10n.sync_auto_12_hours: 43200, + }; return Scaffold( appBar: AppBar( title: Text(l10nLocalizations(context)!.syncing), @@ -56,7 +68,7 @@ class SyncScreen extends ConsumerWidget { builder: (context) { return AlertDialog( title: Text( - l10n.app_language, + l10n.sync_auto, ), content: SizedBox( width: context.width(0.8), @@ -64,20 +76,23 @@ class SyncScreen extends ConsumerWidget { shrinkWrap: true, itemCount: autoSyncOptions.length, itemBuilder: (context, index) { - final option = autoSyncOptions[index]; + final optionName = + autoSyncOptions.keys.elementAt(index); + final optionValue = autoSyncOptions.values + .elementAt(index); return RadioListTile( dense: true, contentPadding: const EdgeInsets.all(0), - value: option, - groupValue: 0, + value: optionValue, + groupValue: autoSyncFrequency, onChanged: (value) { - /*ref - .read(l10nLocaleStateProvider + ref + .read(synchingProvider(syncId: 1) .notifier) - .setLocale(locale);*/ + .setAutoSyncFrequency(value!); Navigator.pop(context); }, - title: Text(completeLanguageName("")), + title: Text(optionName), ); }, )), @@ -100,13 +115,34 @@ class SyncScreen extends ConsumerWidget { ); }); }, - title: Text(l10n.app_language), + title: Text(l10n.sync_auto), subtitle: Text( - completeLanguageName(""), + autoSyncOptions.entries + .where((o) => o.value == autoSyncFrequency) + .first + .key, style: TextStyle( fontSize: 11, color: context.secondaryColor), ), ), + ListTile( + title: Padding( + padding: const EdgeInsets.only(bottom: 8), + child: Row( + children: [ + Icon( + Icons.warning_amber_outlined, + color: context.secondaryColor, + ), + const SizedBox(width: 10), + Text(l10n.sync_auto_warning, + softWrap: true, + style: TextStyle( + fontSize: 11, color: context.secondaryColor)) + ], + ), + ), + ), Padding( padding: const EdgeInsets.only( left: 15, right: 15, bottom: 10, top: 5), @@ -531,13 +567,197 @@ class SyncScreen extends ConsumerWidget { ElevatedButton( onPressed: !isLogged ? null - : () { - ref + : () async { + final snapshots = await ref .read(syncServerProvider(syncId: 1) .notifier) .getSnapshots(l10n); + if (context.mounted) { + showDialog( + context: context, + builder: (context) { + return AlertDialog( + title: Text( + l10n.sync_snapshots, + ), + content: SizedBox( + width: context.width(0.8), + child: ListView.builder( + shrinkWrap: true, + itemCount: snapshots.length, + itemBuilder: + (context, index) { + return Padding( + padding: + const EdgeInsets + .symmetric( + horizontal: 5), + child: Card( + child: Column( + children: [ + ElevatedButton( + style: ElevatedButton.styleFrom( + backgroundColor: + Colors + .transparent, + elevation: + 0, + shadowColor: + Colors + .transparent, + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.only( + bottomLeft: Radius.circular(0), + bottomRight: Radius.circular(0), + topRight: Radius.circular(10), + topLeft: Radius.circular(10)))), + onPressed: () {}, + child: Row( + crossAxisAlignment: + CrossAxisAlignment + .end, + children: [ + const Icon( + Icons + .save), + const SizedBox( + width: + 10, + ), + Expanded( + child: + Text("${dateFormat((snapshots[index].createdAt!).toString(), ref: ref, context: context)} ${dateFormatHour((snapshots[index].createdAt!).toString(), context)}")) + ], + )), + Row( + mainAxisAlignment: + MainAxisAlignment + .end, + children: [ + Row( + children: [ + IconButton( + onPressed: + () { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Text( + l10n.sync_load_snapshot, + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(l10n.cancel)), + const SizedBox( + width: 15, + ), + TextButton( + onPressed: () async { + await ref.read(SyncServerProvider(syncId: 1).notifier).downloadSnapshot(l10n, snapshots[index].uuid!); + if (context.mounted) { + Navigator.pop(context); + } + }, + child: Text( + l10n.ok, + )), + ], + ) + ], + ); + }, + ); + }); + }, + icon: + const Icon(Icons.cloud_download_outlined)), + IconButton( + onPressed: + () { + showDialog( + context: context, + builder: (context) { + return StatefulBuilder( + builder: (context, setState) { + return AlertDialog( + title: Text( + l10n.sync_delete_snapshot, + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () { + Navigator.pop(context); + }, + child: Text(l10n.cancel)), + const SizedBox( + width: 15, + ), + TextButton( + onPressed: () async { + await ref.read(syncServerProvider(syncId: 1).notifier).deleteSnapshot(l10n, snapshots[index].uuid!); + if (context.mounted) { + Navigator.pop(context); + } + }, + child: Text( + l10n.ok, + )), + ], + ) + ], + ); + }, + ); + }); + }, + icon: + const Icon(Icons.delete_outlined)) + ], + ), + ], + ) + ], + ), + ), + ); + }, + )), + actions: [ + Row( + mainAxisAlignment: + MainAxisAlignment.end, + children: [ + TextButton( + onPressed: () async { + Navigator.pop( + context); + }, + child: Text( + l10n.cancel, + style: TextStyle( + color: context + .primaryColor), + )), + ], + ) + ], + ); + }); + } }, - child: Text("Browse / Download older backups")), + child: Text(l10n.sync_browse_snapshots)), ], ), ), diff --git a/lib/services/fetch_sources_list.dart b/lib/services/fetch_sources_list.dart index f5beb7bc..3a08eda4 100644 --- a/lib/services/fetch_sources_list.dart +++ b/lib/services/fetch_sources_list.dart @@ -173,9 +173,11 @@ void checkIfSourceIsObsolete( sourceList.where((e) => e.id != null).map((e) => e.id).toList(); if (ids.isNotEmpty) { isar.writeTxnSync(() { + if (source.isObsolete != !ids.contains(source.id)) { + ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( + ActionType.updateExtension, source.id, source.toJson(), false); + } isar.sources.putSync(source..isObsolete = !ids.contains(source.id)); - ref.read(synchingProvider(syncId: 1).notifier).addChangedPart( - ActionType.updateExtension, source.id, source.toJson(), false); }); } } diff --git a/lib/services/sync_server.dart b/lib/services/sync_server.dart index 8058af18..de69052c 100644 --- a/lib/services/sync_server.dart +++ b/lib/services/sync_server.dart @@ -97,7 +97,7 @@ class SyncServer extends _$SyncServer { final syncNotifier = ref.read(synchingProvider(syncId: syncId).notifier); syncNotifier.setLastSync(DateTime.now().millisecondsSinceEpoch); - syncNotifier.clearAllChangedParts(); + syncNotifier.clearAllChangedParts(true); ref.invalidate(synchingProvider(syncId: syncId)); botToast(l10n.sync_download_finished, second: 2); @@ -136,7 +136,7 @@ class SyncServer extends _$SyncServer { } } - Future getSnapshots(AppLocalizations l10n) async { + Future> getSnapshots(AppLocalizations l10n) async { try { final accessToken = _getAccessToken(); @@ -151,10 +151,10 @@ class SyncServer extends _$SyncServer { botToast(l10n.server_error, second: 5); return List.empty(); } - var snapshots = jsonDecode(response.body) as List; - for (final snapshot in snapshots) { - print( - "${snapshot["id"]} - ${DateTime.parse(snapshot["dbCreatedAt"]).millisecondsSinceEpoch}"); + var temp = jsonDecode(response.body) as List; + final snapshots = List.empty(growable: true); + for (final snapshot in temp) { + snapshots.add(Snapshot.fromJson(snapshot)); } return snapshots; } catch (error) { @@ -221,6 +221,28 @@ class SyncServer extends _$SyncServer { } } + Future deleteSnapshot(AppLocalizations l10n, String snapshotId) async { + botToast(l10n.sync_snapshot_deleting, second: 2); + try { + final accessToken = _getAccessToken(); + + var response = await http.delete( + Uri.parse('${_getServer()}$_snapshotUrl/$snapshotId'), + headers: { + 'Content-Type': 'application/json', + 'Authorization': 'Bearer $accessToken' + }, + ); + if (response.statusCode != 200) { + botToast(l10n.server_error, second: 5); + return; + } + botToast(l10n.sync_snapshot_deleted, second: 2); + } catch (error) { + botToast(error.toString(), second: 5); + } + } + Future uploadToServer(AppLocalizations l10n) async { botToast(l10n.sync_uploading, second: 2); try { @@ -239,12 +261,11 @@ class SyncServer extends _$SyncServer { botToast(l10n.sync_upload_failed, second: 5); return; } - ref - .read(synchingProvider(syncId: syncId).notifier) - .setLastUpload(DateTime.now().millisecondsSinceEpoch); - ref - .read(synchingProvider(syncId: syncId).notifier) - .clearAllChangedParts(); + + final syncNotifier = ref.read(synchingProvider(syncId: syncId).notifier); + syncNotifier.setLastUpload(DateTime.now().millisecondsSinceEpoch); + syncNotifier.clearAllChangedParts(true); + ref.invalidate(synchingProvider(syncId: syncId)); botToast(l10n.sync_upload_finished, second: 2); } catch (error) { @@ -407,3 +428,14 @@ class SyncServer extends _$SyncServer { return syncPrefs.server ?? ""; } } + +class Snapshot { + String? uuid; + int? createdAt; + Snapshot({required this.uuid, required this.createdAt}); + + Snapshot.fromJson(Map json) { + uuid = json['id']; + createdAt = DateTime.parse(json["dbCreatedAt"]).millisecondsSinceEpoch; + } +} diff --git a/lib/services/sync_server.g.dart b/lib/services/sync_server.g.dart index e0cecd66..b6d9039b 100644 --- a/lib/services/sync_server.g.dart +++ b/lib/services/sync_server.g.dart @@ -6,7 +6,7 @@ part of 'sync_server.dart'; // RiverpodGenerator // ************************************************************************** -String _$syncServerHash() => r'b1d19a01737badaa79b42a7a39f2f7782463ee63'; +String _$syncServerHash() => r'd13b5d6eaded02a9256e64550e98983b17ab70f4'; /// Copied from Dart SDK class _SystemHash {