diff --git a/app/lib/controller/files_controller.dart b/app/lib/controller/files_controller.dart index c858dca2..e437498b 100644 --- a/app/lib/controller/files_controller.dart +++ b/app/lib/controller/files_controller.dart @@ -13,6 +13,7 @@ import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/progress_util.dart'; import 'package:nc_photos/rx_extension.dart'; import 'package:nc_photos/use_case/file/list_file.dart'; +import 'package:nc_photos/use_case/find_file_descriptor.dart'; import 'package:nc_photos/use_case/remove.dart'; import 'package:nc_photos/use_case/sync_dir.dart'; import 'package:nc_photos/use_case/update_property.dart'; @@ -20,6 +21,7 @@ import 'package:np_codegen/np_codegen.dart'; import 'package:np_common/lazy.dart'; import 'package:np_common/object_util.dart'; import 'package:np_common/or_null.dart'; +import 'package:np_db/np_db.dart'; import 'package:rxdart/rxdart.dart'; import 'package:to_string/to_string.dart'; @@ -233,6 +235,69 @@ class FilesController { } } + Future applySyncResult({ + DbSyncIdResult? favorites, + List? fileExifs, + }) async { + if (favorites?.isNotEmpty != true && fileExifs?.isNotEmpty != true) { + return; + } + + // do async ops first + final fileExifFiles = + await fileExifs?.letFuture((e) async => await FindFileDescriptor(_c)( + account, + e, + onFileNotFound: (id) { + _log.warning("[applySyncResult] File id not found: $id"); + }, + )); + + final next = LinkedHashMap.of(_dataStreamController.value.files); + if (favorites != null && favorites.isNotEmpty) { + _applySyncFavoriteResult(next, favorites); + } + if (fileExifFiles != null && fileExifFiles.isNotEmpty) { + _applySyncFileExifResult(next, fileExifFiles); + } + _dataStreamController.addWithValue((value) => value.copyWith(files: next)); + } + + void _applySyncFavoriteResult( + Map next, DbSyncIdResult result) { + for (final id in result.insert) { + final f = next[id]; + if (f == null) { + _log.warning("[_applySyncFavoriteResult] File id not found: $id"); + continue; + } + if (f is File) { + next[id] = f.copyWith(isFavorite: true); + } else { + next[id] = f.copyWith(fdIsFavorite: true); + } + } + for (final id in result.delete) { + final f = next[id]; + if (f == null) { + _log.warning("[_applySyncFavoriteResult] File id not found: $id"); + continue; + } + if (f is File) { + next[id] = f.copyWith(isFavorite: false); + } else { + next[id] = f.copyWith(fdIsFavorite: false); + } + } + } + + void _applySyncFileExifResult( + Map next, List results) { + for (final f in results) { + next[f.fdId] = f; + } + } + Future _load() async { var lastData = _FilesStreamEvent( files: const {}, diff --git a/app/lib/controller/sync_controller.dart b/app/lib/controller/sync_controller.dart index a394ac7d..2552cd97 100644 --- a/app/lib/controller/sync_controller.dart +++ b/app/lib/controller/sync_controller.dart @@ -2,6 +2,8 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:nc_photos/account.dart'; +import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/use_case/startup_sync.dart'; @@ -15,14 +17,19 @@ class SyncController { _isDisposed = true; } - Future requestSync( - Account account, PersonProvider personProvider) async { + Future requestSync({ + required Account account, + required FilesController filesController, + required PersonsController personsController, + required PersonProvider personProvider, + }) async { if (_isDisposed) { return; } if (_syncCompleter == null) { _syncCompleter = Completer(); - final result = await StartupSync.runInIsolate(account, personProvider); + final result = await StartupSync.runInIsolate( + account, filesController, personsController, personProvider); if (!_isDisposed && result.isSyncPersonUpdated) { onPeopleUpdated?.call(); } @@ -32,15 +39,23 @@ class SyncController { } } - Future requestResync( - Account account, PersonProvider personProvider) async { + Future requestResync({ + required Account account, + required FilesController filesController, + required PersonsController personsController, + required PersonProvider personProvider, + }) async { if (_syncCompleter?.isCompleted == true) { _syncCompleter = null; - return requestSync(account, personProvider); } else { // already syncing - return requestSync(account, personProvider); } + return requestSync( + account: account, + filesController: filesController, + personsController: personsController, + personProvider: personProvider, + ); } final Account account; diff --git a/app/lib/use_case/cache_favorite.dart b/app/lib/use_case/cache_favorite.dart index bc6756f0..ab70e247 100644 --- a/app/lib/use_case/cache_favorite.dart +++ b/app/lib/use_case/cache_favorite.dart @@ -6,6 +6,7 @@ import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/event/event.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_db/np_db.dart'; part 'cache_favorite.g.dart'; @@ -15,18 +16,18 @@ class CacheFavorite { /// Cache favorites using results from remote /// - /// Return number of files updated - Future call(Account account, Iterable remoteFileIds) async { + /// Return the fileIds of the affected files + Future call( + Account account, Iterable remoteFileIds) async { _log.info("[call] Cache favorites"); final result = await _c.npDb.syncFavoriteFiles( account: account.toDb(), favoriteFileIds: remoteFileIds.toList(), ); - final count = result.insert + result.delete + result.update; - if (count > 0) { + if (result.isNotEmpty) { KiwiContainer().resolve().fire(FavoriteResyncedEvent(account)); } - return count; + return result; } final DiContainer _c; diff --git a/app/lib/use_case/startup_sync.dart b/app/lib/use_case/startup_sync.dart index cafabc00..6d8546f5 100644 --- a/app/lib/use_case/startup_sync.dart +++ b/app/lib/use_case/startup_sync.dart @@ -7,6 +7,8 @@ import 'package:logging/logging.dart'; import 'package:mutex/mutex.dart'; import 'package:nc_photos/account.dart'; import 'package:nc_photos/app_init.dart' as app_init; +import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/entity/person.dart'; import 'package:nc_photos/event/event.dart'; @@ -14,8 +16,11 @@ import 'package:nc_photos/use_case/person/sync_person.dart'; import 'package:nc_photos/use_case/sync_favorite.dart'; import 'package:nc_photos/use_case/sync_tag.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_common/object_util.dart'; import 'package:np_common/type.dart'; +import 'package:np_db/np_db.dart'; import 'package:np_platform_util/np_platform_util.dart'; +import 'package:to_string/to_string.dart'; part 'startup_sync.g.dart'; @@ -28,7 +33,11 @@ class StartupSync { /// Sync in a background isolate static Future runInIsolate( - Account account, PersonProvider personProvider) async { + Account account, + FilesController filesController, + PersonsController personsController, + PersonProvider personProvider, + ) async { return _mutex.protect(() async { if (getRawPlatform() == NpPlatform.web) { // not supported on web @@ -42,7 +51,7 @@ class StartupSync { final result = SyncResult.fromJson(resultJson); // events fired in background isolate won't be noticed by the main isolate, // so we fire them again here - _broadcastResult(account, result); + _broadcastResult(account, filesController, personsController, result); return result; } }); @@ -52,16 +61,16 @@ class StartupSync { Account account, PersonProvider personProvider) async { _log.info("[_run] Begin sync"); final stopwatch = Stopwatch()..start(); - late final int syncFavoriteCount; - late final bool isSyncPersonUpdated; + DbSyncIdResult? syncFavoriteResult; + DbSyncIdResult? syncTagResult; + var isSyncPersonUpdated = false; try { - syncFavoriteCount = await SyncFavorite(_c)(account); + syncFavoriteResult = await SyncFavorite(_c)(account); } catch (e, stackTrace) { _log.shout("[_run] Failed while SyncFavorite", e, stackTrace); - syncFavoriteCount = -1; } try { - await SyncTag(_c)(account); + syncTagResult = await SyncTag(_c)(account); } catch (e, stackTrace) { _log.shout("[_run] Failed while SyncTag", e, stackTrace); } @@ -72,16 +81,28 @@ class StartupSync { } _log.info("[_run] Elapsed time: ${stopwatch.elapsedMilliseconds}ms"); return SyncResult( - syncFavoriteCount: syncFavoriteCount, + syncFavoriteResult: syncFavoriteResult, + syncTagResult: syncTagResult, isSyncPersonUpdated: isSyncPersonUpdated, ); } - static void _broadcastResult(Account account, SyncResult result) { - final eventBus = KiwiContainer().resolve(); - if (result.syncFavoriteCount > 0) { + static void _broadcastResult( + Account account, + FilesController filesController, + PersonsController personsController, + SyncResult result, + ) { + _$StartupSyncNpLog.log.info('[_broadcastResult] $result'); + if (result.syncFavoriteResult != null) { + filesController.applySyncResult(favorites: result.syncFavoriteResult!); + // legacy + final eventBus = KiwiContainer().resolve(); eventBus.fire(FavoriteResyncedEvent(account)); } + if (result.isSyncPersonUpdated) { + personsController.reload(); + } } final DiContainer _c; @@ -89,23 +110,35 @@ class StartupSync { static final _mutex = Mutex(); } +@toString class SyncResult { const SyncResult({ - required this.syncFavoriteCount, + required this.syncFavoriteResult, + required this.syncTagResult, required this.isSyncPersonUpdated, }); factory SyncResult.fromJson(JsonObj json) => SyncResult( - syncFavoriteCount: json["syncFavoriteCount"], + syncFavoriteResult: (json["syncFavoriteResult"] as Map?) + ?.cast() + .let(DbSyncIdResult.fromJson), + syncTagResult: (json["syncTagResult"] as Map?) + ?.cast() + .let(DbSyncIdResult.fromJson), isSyncPersonUpdated: json["isSyncPersonUpdated"], ); JsonObj toJson() => { - "syncFavoriteCount": syncFavoriteCount, + "syncFavoriteResult": syncFavoriteResult?.toJson(), + "syncTagResult": syncTagResult?.toJson(), "isSyncPersonUpdated": isSyncPersonUpdated, }; - final int syncFavoriteCount; + @override + String toString() => _$toString(); + + final DbSyncIdResult? syncFavoriteResult; + final DbSyncIdResult? syncTagResult; final bool isSyncPersonUpdated; } diff --git a/app/lib/use_case/startup_sync.g.dart b/app/lib/use_case/startup_sync.g.dart index 7e45e33d..ca0cf08e 100644 --- a/app/lib/use_case/startup_sync.g.dart +++ b/app/lib/use_case/startup_sync.g.dart @@ -12,3 +12,14 @@ extension _$StartupSyncNpLog on StartupSync { static final log = Logger("use_case.startup_sync.StartupSync"); } + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$SyncResultToString on SyncResult { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "SyncResult {syncFavoriteResult: $syncFavoriteResult, syncTagResult: $syncTagResult, isSyncPersonUpdated: $isSyncPersonUpdated}"; + } +} diff --git a/app/lib/use_case/sync_favorite.dart b/app/lib/use_case/sync_favorite.dart index 02175043..d8a52b3d 100644 --- a/app/lib/use_case/sync_favorite.dart +++ b/app/lib/use_case/sync_favorite.dart @@ -6,6 +6,7 @@ import 'package:nc_photos/entity/file_util.dart' as file_util; import 'package:nc_photos/entity/pref.dart'; import 'package:nc_photos/use_case/cache_favorite.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_db/np_db.dart'; part 'sync_favorite.g.dart'; @@ -18,7 +19,7 @@ class SyncFavorite { /// Sync favorites in cache db with remote server /// /// Return number of files updated - Future call(Account account) async { + Future call(Account account) async { _log.info("[call] Sync favorites with remote"); final remote = await _getRemoteFavoriteFileIds(account); return await CacheFavorite(_c)(account, remote); diff --git a/app/lib/use_case/sync_tag.dart b/app/lib/use_case/sync_tag.dart index 7234c030..415933a6 100644 --- a/app/lib/use_case/sync_tag.dart +++ b/app/lib/use_case/sync_tag.dart @@ -3,6 +3,7 @@ import 'package:nc_photos/account.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:np_codegen/np_codegen.dart'; +import 'package:np_db/np_db.dart'; part 'sync_tag.g.dart'; @@ -11,13 +12,16 @@ class SyncTag { const SyncTag(this._c); /// Sync tags in cache db with remote server - Future call(Account account) async { + /// + /// Return tagIds of the affected tags + Future call(Account account) async { _log.info("[call] Sync tags with remote"); final remote = await _c.tagRepoRemote.list(account); - await _c.npDb.syncTags( + final result = await _c.npDb.syncTags( account: account.toDb(), tags: remote.map(DbTagConverter.toDb).toList(), ); + return result; } final DiContainer _c; diff --git a/app/lib/widget/home_photos.dart b/app/lib/widget/home_photos.dart index e83036ed..40bb1ebc 100644 --- a/app/lib/widget/home_photos.dart +++ b/app/lib/widget/home_photos.dart @@ -609,7 +609,11 @@ class _HomePhotosState extends State if (isPostSuccess) { _isScrollbarVisible = true; context.read().syncController.requestSync( - widget.account, _accountPrefController.personProvider.value); + account: widget.account, + filesController: context.read(), + personsController: context.read(), + personProvider: _accountPrefController.personProvider.value, + ); _tryStartMetadataTask(context); } }); diff --git a/app/lib/widget/home_photos/bloc.dart b/app/lib/widget/home_photos/bloc.dart index 4ed8ad3e..c7715cb7 100644 --- a/app/lib/widget/home_photos/bloc.dart +++ b/app/lib/widget/home_photos/bloc.dart @@ -10,6 +10,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { required this.accountPrefController, required this.collectionsController, required this.sessionController, + required this.syncController, + required this.personsController, }) : super(_State.init( zoom: prefController.homePhotosZoomLevel.value, isEnableMemoryCollection: @@ -142,6 +144,12 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { memoryCollections: ev.memoryCollections, isLoading: _itemTransformerQueue.isProcessing, )); + syncController.requestSync( + account: account, + filesController: controller, + personsController: personsController, + personProvider: accountPrefController.personProvider.value, + ); _tryStartMetadataTask(); } @@ -403,6 +411,8 @@ class _Bloc extends Bloc<_Event, _State> with BlocLogger { final AccountPrefController accountPrefController; final CollectionsController collectionsController; final SessionController sessionController; + final SyncController syncController; + final PersonsController personsController; final _itemTransformerQueue = ComputeQueue<_ItemTransformerArgument, _ItemTransformerResult>(); diff --git a/app/lib/widget/home_photos2.dart b/app/lib/widget/home_photos2.dart index db204002..22579630 100644 --- a/app/lib/widget/home_photos2.dart +++ b/app/lib/widget/home_photos2.dart @@ -18,8 +18,10 @@ import 'package:nc_photos/controller/account_controller.dart'; import 'package:nc_photos/controller/account_pref_controller.dart'; import 'package:nc_photos/controller/collections_controller.dart'; import 'package:nc_photos/controller/files_controller.dart'; +import 'package:nc_photos/controller/persons_controller.dart'; import 'package:nc_photos/controller/pref_controller.dart'; import 'package:nc_photos/controller/session_controller.dart'; +import 'package:nc_photos/controller/sync_controller.dart'; import 'package:nc_photos/db/entity_converter.dart'; import 'package:nc_photos/di_container.dart'; import 'package:nc_photos/download_handler.dart'; @@ -87,6 +89,8 @@ class HomePhotos2 extends StatelessWidget { accountPrefController: accountController.accountPrefController, collectionsController: accountController.collectionsController, sessionController: accountController.sessionController, + syncController: accountController.syncController, + personsController: accountController.personsController, ), child: const _WrappedHomePhotos(), ); diff --git a/app/lib/widget/settings/account_settings.dart b/app/lib/widget/settings/account_settings.dart index 64d704a7..7f9a7150 100644 --- a/app/lib/widget/settings/account_settings.dart +++ b/app/lib/widget/settings/account_settings.dart @@ -109,8 +109,12 @@ class _WrappedAccountSettingsState extends State<_WrappedAccountSettings> if (_bloc.state.shouldResync && _bloc.state.personProvider != PersonProvider.none) { _log.fine("[dispose] Requesting to resync account"); - _accountController.syncController - .requestResync(_bloc.state.account, _bloc.state.personProvider); + _accountController.syncController.requestResync( + account: _bloc.state.account, + filesController: context.read(), + personsController: context.read(), + personProvider: _bloc.state.personProvider, + ); } _animationController.dispose(); super.dispose(); diff --git a/np_db/lib/src/api.dart b/np_db/lib/src/api.dart index f99ef2ae..42b8fc39 100644 --- a/np_db/lib/src/api.dart +++ b/np_db/lib/src/api.dart @@ -49,6 +49,36 @@ class DbSyncResult { final int update; } +/// Sync results with ids +/// +/// The meaning of the ids returned depends on the specific call +class DbSyncIdResult { + const DbSyncIdResult({ + required this.insert, + required this.delete, + required this.update, + }); + + factory DbSyncIdResult.fromJson(JsonObj json) => DbSyncIdResult( + insert: (json["insert"] as List).cast(), + delete: (json["delete"] as List).cast(), + update: (json["update"] as List).cast(), + ); + + JsonObj toJson() => { + "insert": insert, + "delete": delete, + "update": update, + }; + + bool get isEmpty => insert.isEmpty && delete.isEmpty && update.isEmpty; + bool get isNotEmpty => !isEmpty; + + final List insert; + final List delete; + final List update; +} + @toString class DbLocationGroup with EquatableMixin { const DbLocationGroup({ @@ -235,7 +265,9 @@ abstract class NpDb { }); /// Add or replace nc albums in db - Future syncFavoriteFiles({ + /// + /// Return fileIds affected by this call + Future syncFavoriteFiles({ required DbAccount account, required List favoriteFileIds, }); @@ -374,7 +406,7 @@ abstract class NpDb { }); /// Replace all tags for [account] - Future syncTags({ + Future syncTags({ required DbAccount account, required List tags, }); diff --git a/np_db_sqlite/lib/src/sqlite_api.dart b/np_db_sqlite/lib/src/sqlite_api.dart index 67cabdeb..25c5d97e 100644 --- a/np_db_sqlite/lib/src/sqlite_api.dart +++ b/np_db_sqlite/lib/src/sqlite_api.dart @@ -337,7 +337,7 @@ class NpDbSqlite implements NpDb { } @override - Future syncFavoriteFiles({ + Future syncFavoriteFiles({ required DbAccount account, required List favoriteFileIds, }) async { @@ -370,10 +370,10 @@ class NpDbSqlite implements NpDb { isFavorite: const OrNull(false), ); } - return DbSyncResult( - insert: inserts.length, - delete: deletes.length, - update: 0, + return DbSyncIdResult( + insert: inserts, + delete: deletes, + update: const [], ); }); } @@ -834,7 +834,7 @@ class NpDbSqlite implements NpDb { } @override - Future syncTags({ + Future syncTags({ required DbAccount account, required List tags, }) async { @@ -863,10 +863,10 @@ class NpDbSqlite implements NpDb { updates: updates, ); } - return DbSyncResult( - insert: inserts.length, - delete: deletes.length, - update: updates.length, + return DbSyncIdResult( + insert: inserts.map((e) => e.id).toList(), + delete: deletes.map((e) => e.id).toList(), + update: updates.map((e) => e.id).toList(), ); }); }