From 6d6cd91b22c0152fe7a6287db44f1d29da1d86ce Mon Sep 17 00:00:00 2001 From: Prateek Sunal Date: Tue, 15 Jul 2025 16:19:11 +0530 Subject: [PATCH] fix: optimize things (1) --- mobile/apps/photos/lib/app.dart | 4 +- mobile/apps/photos/lib/db/entities_db.dart | 17 ++ .../models/collection/smart_album_config.dart | 109 ----------- mobile/apps/photos/lib/service_locator.dart | 4 - .../photos/lib/services/entity_service.dart | 7 + .../lib/services/smart_albums_service.dart | 178 ++++++++++++++++++ .../lib/services/sync/sync_service.dart | 4 +- .../collections/album/smart_album_people.dart | 40 +++- 8 files changed, 238 insertions(+), 125 deletions(-) create mode 100644 mobile/apps/photos/lib/services/smart_albums_service.dart diff --git a/mobile/apps/photos/lib/app.dart b/mobile/apps/photos/lib/app.dart index 3ff91b4af8..36d91639a5 100644 --- a/mobile/apps/photos/lib/app.dart +++ b/mobile/apps/photos/lib/app.dart @@ -15,12 +15,12 @@ import "package:photos/events/memories_changed_event.dart"; import "package:photos/events/people_changed_event.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/l10n/l10n.dart"; -import "package:photos/models/collection/smart_album_config.dart"; import "package:photos/service_locator.dart"; import 'package:photos/services/app_lifecycle_service.dart'; import "package:photos/services/home_widget_service.dart"; import "package:photos/services/memory_home_widget_service.dart"; import "package:photos/services/people_home_widget_service.dart"; +import "package:photos/services/smart_albums_service.dart"; import 'package:photos/services/sync/sync_service.dart'; import 'package:photos/ui/tabs/home_widget.dart'; import "package:photos/ui/viewer/actions/file_viewer.dart"; @@ -77,7 +77,7 @@ class _EnteAppState extends State with WidgetsBindingObserver { _changeCallbackDebouncer.run( () async { unawaited(PeopleHomeWidgetService.instance.checkPeopleChanged()); - unawaited(syncSmartAlbums()); + unawaited(SmartAlbumsService.instance.syncSmartAlbums()); }, ); }, diff --git a/mobile/apps/photos/lib/db/entities_db.dart b/mobile/apps/photos/lib/db/entities_db.dart index fba74d98a5..06aa3501d4 100644 --- a/mobile/apps/photos/lib/db/entities_db.dart +++ b/mobile/apps/photos/lib/db/entities_db.dart @@ -128,4 +128,21 @@ extension EntitiesDB on FilesDB { } return maps.values.first as String?; } + + Future> getUpdatedAts( + EntityType type, + List ids, + ) async { + final db = await sqliteAsyncDB; + final List> maps = await db.getAll( + 'SELECT id, updatedAt FROM entities WHERE type = ? AND id IN (${List.filled(ids.length, '?').join(',')})', + [type.name, ...ids], + ); + return Map.fromEntries( + List.generate( + maps.length, + (i) => MapEntry(maps[i]['id'] as String, maps[i]['updatedAt'] as int), + ), + ); + } } diff --git a/mobile/apps/photos/lib/models/collection/smart_album_config.dart b/mobile/apps/photos/lib/models/collection/smart_album_config.dart index 461f7869c6..7f7abf4b03 100644 --- a/mobile/apps/photos/lib/models/collection/smart_album_config.dart +++ b/mobile/apps/photos/lib/models/collection/smart_album_config.dart @@ -1,12 +1,3 @@ -import "package:photos/models/api/entity/type.dart"; -import "package:photos/service_locator.dart" show entityService, prefs; -import "package:photos/services/collections_service.dart"; -import "package:photos/services/search_service.dart"; -import "package:photos/ui/actions/collection/collection_file_actions.dart"; -import "package:photos/ui/actions/collection/collection_sharing_actions.dart"; -import "package:shared_preferences/shared_preferences.dart"; -import "package:synchronized/synchronized.dart"; - class SmartAlbumConfig { final int collectionId; // person ids @@ -20,9 +11,6 @@ class SmartAlbumConfig { required this.addedFiles, }); - static const _personIdsKey = "smart_album_person_ids"; - static const _addedFilesKey = "smart_album_added_files"; - Future getUpdatedConfig(Set newPersonsIds) async { final toAdd = newPersonsIds.difference(personIDs); final toRemove = personIDs.difference(newPersonsIds); @@ -65,101 +53,4 @@ class SmartAlbumConfig { addedFiles: newFiles, ); } - - Future saveConfig() async { - final prefs = await SharedPreferences.getInstance(); - - await prefs.setStringList( - "${_personIdsKey}_$collectionId", - personIDs.toList(), - ); - - await prefs.setString( - "${_addedFilesKey}_$collectionId", - addedFiles.entries - .map((e) => "${e.key}:${e.value.$1}|${e.value.$2.join(',')}") - .join(';'), - ); - } - - static Future loadConfig(int collectionId) async { - final personIDs = - prefs.getStringList("${_personIdsKey}_$collectionId") ?? []; - final addedFilesString = - prefs.getString("${_addedFilesKey}_$collectionId") ?? ""; - - final addedFiles = )>{}; - if (addedFilesString.isNotEmpty) { - for (final entry in addedFilesString.split(';')) { - final parts = entry.split(':'); - if (parts.length == 2) { - addedFiles[parts[0]] = ( - int.parse(parts[1].split('|')[0]), - parts[1].split('|')[1].split(',').map(int.parse).toSet(), - ); - } - } - } - - return SmartAlbumConfig( - collectionId: collectionId, - personIDs: personIDs.toSet(), - addedFiles: addedFiles, - ); - } -} - -final _lock = Lock(); - -Future syncSmartAlbums() async { - await _lock.synchronized(() async { - // get all collections - final collections = CollectionsService.instance.nonHiddenOwnedCollections(); - for (final collectionId in collections) { - final config = await SmartAlbumConfig.loadConfig(collectionId); - - if (config.personIDs.isEmpty) { - continue; - } - - for (final personId in config.personIDs) { - final person = - await entityService.getEntity(EntityType.cgroup, personId); - - if (person == null || - config.addedFiles[personId]?.$1 == null || - (person.updatedAt <= config.addedFiles[personId]!.$1)) { - continue; - } - - final files = - (await SearchService.instance.getClusterFilesForPersonID(personId)) - .entries - .expand((e) => e.value) - .toSet(); - - final toBeSynced = - files.difference(config.addedFiles[personId]?.$2 ?? {}); - - if (toBeSynced.isNotEmpty) { - final CollectionActions collectionActions = - CollectionActions(CollectionsService.instance); - final result = await collectionActions.addToCollection( - null, - collectionId, - false, - selectedFiles: toBeSynced.toList(), - ); - if (result) { - final newConfig = await config.addFiles( - personId, - person.updatedAt, - toBeSynced.map((e) => e.uploadedFileID!).toSet(), - ); - await newConfig.saveConfig(); - } - } - } - } - }); } diff --git a/mobile/apps/photos/lib/service_locator.dart b/mobile/apps/photos/lib/service_locator.dart index 35b1e31309..bc96877eb2 100644 --- a/mobile/apps/photos/lib/service_locator.dart +++ b/mobile/apps/photos/lib/service_locator.dart @@ -45,10 +45,6 @@ class ServiceLocator { } } -SharedPreferences get prefs { - return ServiceLocator.instance.prefs; -} - FlagService? _flagService; FlagService get flagService { diff --git a/mobile/apps/photos/lib/services/entity_service.dart b/mobile/apps/photos/lib/services/entity_service.dart index 6c317b1fe4..39c7e49d09 100644 --- a/mobile/apps/photos/lib/services/entity_service.dart +++ b/mobile/apps/photos/lib/services/entity_service.dart @@ -249,4 +249,11 @@ class EntityService { final hash = md5.convert(utf8.encode(preHash)).toString().substring(0, 10); return hash; } + + Future> getUpdatedAts( + EntityType type, + List personIds, + ) async { + return await _db.getUpdatedAts(type, personIds); + } } diff --git a/mobile/apps/photos/lib/services/smart_albums_service.dart b/mobile/apps/photos/lib/services/smart_albums_service.dart new file mode 100644 index 0000000000..d6eb6a3032 --- /dev/null +++ b/mobile/apps/photos/lib/services/smart_albums_service.dart @@ -0,0 +1,178 @@ +import "package:logging/logging.dart"; +import "package:photos/models/api/entity/type.dart"; +import "package:photos/models/collection/smart_album_config.dart"; +import "package:photos/service_locator.dart" show entityService, ServiceLocator; +import "package:photos/services/collections_service.dart"; +import "package:photos/services/search_service.dart"; +import "package:photos/ui/actions/collection/collection_file_actions.dart"; +import "package:photos/ui/actions/collection/collection_sharing_actions.dart"; +import "package:shared_preferences/shared_preferences.dart"; +import "package:synchronized/synchronized.dart"; + +class SmartAlbumsService { + SmartAlbumsService._(); + + static final SmartAlbumsService instance = SmartAlbumsService._(); + + final _lock = Lock(); + final _logger = Logger((SmartAlbumsService).toString()); + + final Map _cachedConfigs = {}; + bool isInitialized = false; + + void init() { + _logger.info("SmartAlbumsService initialized"); + refresh().ignore(); + } + + Future refresh() async { + await _lock.synchronized(() async { + if (isInitialized) return; + + _logger.info("Refreshing SmartAlbumsService"); + + final collections = + CollectionsService.instance.nonHiddenOwnedCollections(); + + for (final collectionId in collections) { + try { + final config = await loadConfig(collectionId); + _cachedConfigs[collectionId] = config; + } catch (_) {} + } + + isInitialized = true; + }); + } + + void updateCachedCollection(SmartAlbumConfig config) => + _cachedConfigs[config.collectionId] = config; + + Future syncSmartAlbums() async { + await _lock.synchronized(() async { + if (!isInitialized) await refresh(); + + for (final entry in _cachedConfigs.entries) { + final collectionId = entry.key; + final config = entry.value; + + final addedFiles = config.addedFiles; + + // Person Id key mapped to updatedAt value + final updatedAtMap = await entityService.getUpdatedAts( + EntityType.cgroup, + config.personIDs.toList(), + ); + + for (final personId in config.personIDs) { + // compares current updateAt with last added file's updatedAt + if (updatedAtMap[personId] == null || + addedFiles[personId] == null || + (updatedAtMap[personId]! <= addedFiles[personId]!.$1)) { + continue; + } + + final files = (await SearchService.instance + .getClusterFilesForPersonID(personId)) + .entries + .expand((e) => e.value) + .toList(); + + // TODO: Can we optimize it more? + final toBeSynced = files + ..removeWhere( + (e) => + e.uploadedFileID == null || + config.addedFiles[personId]!.$2.contains(e.uploadedFileID), + ); + + if (toBeSynced.isNotEmpty) { + final CollectionActions collectionActions = + CollectionActions(CollectionsService.instance); + + final result = await collectionActions.addToCollection( + null, + collectionId, + false, + selectedFiles: toBeSynced, + ); + + if (result) { + final newConfig = await config.addFiles( + personId, + updatedAtMap[personId]!, + toBeSynced.map((e) => e.uploadedFileID!).toSet(), + ); + await saveConfig(newConfig); + } + } + } + } + }); + } + + static const _personIdsKey = "smart_album_person_ids"; + static const _addedFilesKey = "smart_album_added_files"; + + Future saveConfig(SmartAlbumConfig config) async { + final prefs = await SharedPreferences.getInstance(); + + final collectionId = config.collectionId; + final personIDs = config.personIDs; + final addedFiles = config.addedFiles; + + await prefs.setStringList( + "${_personIdsKey}_$collectionId", + personIDs.toList(), + ); + + await prefs.setString( + "${_addedFilesKey}_$collectionId", + addedFiles.entries + .map((e) => "${e.key}:${e.value.$1}|${e.value.$2.join(',')}") + .join(';'), + ); + updateCachedCollection(config); + } + + Future getConfig(int collectionId) async { + if (isInitialized && _cachedConfigs.containsKey(collectionId)) { + return _cachedConfigs[collectionId]!; + } + + refresh().ignore(); + return await loadConfig(collectionId); + } + + Future loadConfig(int collectionId) async { + final personIDs = ServiceLocator.instance.prefs + .getStringList("${_personIdsKey}_$collectionId"); + if (personIDs == null || personIDs.isEmpty) { + throw Exception( + "No person IDs found for collection $collectionId", + ); + } + final addedFilesString = ServiceLocator.instance.prefs + .getString("${_addedFilesKey}_$collectionId") ?? + ""; + + final addedFiles = )>{}; + if (addedFilesString.isNotEmpty) { + for (final entry in addedFilesString.split(';')) { + final parts = entry.split(':'); + if (parts.length == 2) { + addedFiles[parts[0]] = ( + int.parse(parts[1].split('|')[0]), + parts[1].split('|')[1].split(',').map(int.parse).toSet(), + ); + } + } + } + + return SmartAlbumConfig( + collectionId: collectionId, + personIDs: personIDs.toSet(), + addedFiles: addedFiles, + ); + } +} diff --git a/mobile/apps/photos/lib/services/sync/sync_service.dart b/mobile/apps/photos/lib/services/sync/sync_service.dart index 040cb08bb1..f0fda91f91 100644 --- a/mobile/apps/photos/lib/services/sync/sync_service.dart +++ b/mobile/apps/photos/lib/services/sync/sync_service.dart @@ -12,10 +12,10 @@ import 'package:photos/core/event_bus.dart'; import 'package:photos/events/subscription_purchased_event.dart'; import 'package:photos/events/sync_status_update_event.dart'; import 'package:photos/events/trigger_logout_event.dart'; -import "package:photos/models/collection/smart_album_config.dart"; import 'package:photos/models/file/file_type.dart'; import "package:photos/services/language_service.dart"; import 'package:photos/services/notification_service.dart'; +import "package:photos/services/smart_albums_service.dart"; import 'package:photos/services/sync/local_sync_service.dart'; import 'package:photos/services/sync/remote_sync_service.dart'; import 'package:photos/utils/file_uploader.dart'; @@ -200,7 +200,7 @@ class SyncService { if (shouldSync) { await _remoteSyncService.sync(); } - await syncSmartAlbums(); + await SmartAlbumsService.instance.syncSmartAlbums(); } } diff --git a/mobile/apps/photos/lib/ui/collections/album/smart_album_people.dart b/mobile/apps/photos/lib/ui/collections/album/smart_album_people.dart index 9225e559d7..f203843f77 100644 --- a/mobile/apps/photos/lib/ui/collections/album/smart_album_people.dart +++ b/mobile/apps/photos/lib/ui/collections/album/smart_album_people.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import "package:photos/generated/l10n.dart"; import "package:photos/models/collection/smart_album_config.dart"; import "package:photos/models/selected_people.dart"; +import "package:photos/services/smart_albums_service.dart"; import "package:photos/ui/components/buttons/button_widget.dart"; import 'package:photos/ui/components/buttons/icon_button_widget.dart'; import "package:photos/ui/components/models/button_type.dart"; @@ -26,7 +27,7 @@ class SmartAlbumPeople extends StatefulWidget { class _SmartAlbumPeopleState extends State { final _selectedPeople = SelectedPeople(); - late SmartAlbumConfig currentConfig; + SmartAlbumConfig? currentConfig; bool isLoading = false; @override @@ -36,9 +37,12 @@ class _SmartAlbumPeopleState extends State { } Future getSelections() async { - currentConfig = await SmartAlbumConfig.loadConfig(widget.collectionId); + currentConfig = + await SmartAlbumsService.instance.getConfig(widget.collectionId); - _selectedPeople.select(currentConfig.personIDs); + if (currentConfig == null) { + _selectedPeople.select(currentConfig!.personIDs); + } } @override @@ -65,12 +69,32 @@ class _SmartAlbumPeopleState extends State { : () async { isLoading = true; if (mounted) setState(() {}); + try { - final newConfig = await currentConfig.getUpdatedConfig( - _selectedPeople.personIds, - ); - await newConfig.saveConfig(); - syncSmartAlbums().ignore(); + SmartAlbumConfig newConfig; + + if (currentConfig == null) { + final newFiles = )>{}; + + // Add files which are needed + for (final personId in _selectedPeople.personIds) { + newFiles[personId] = (0, {}); + } + + newConfig = SmartAlbumConfig( + collectionId: widget.collectionId, + personIDs: _selectedPeople.personIds, + addedFiles: newFiles, + ); + } else { + newConfig = await currentConfig!.getUpdatedConfig( + _selectedPeople.personIds, + ); + } + + await SmartAlbumsService.instance.saveConfig(newConfig); + SmartAlbumsService.instance.syncSmartAlbums().ignore(); + Navigator.pop(context); } catch (_) { isLoading = false;