fix: optimize things (1)

This commit is contained in:
Prateek Sunal
2025-07-15 16:19:11 +05:30
parent 3708a347f5
commit 6d6cd91b22
8 changed files with 238 additions and 125 deletions

View File

@@ -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<EnteApp> with WidgetsBindingObserver {
_changeCallbackDebouncer.run(
() async {
unawaited(PeopleHomeWidgetService.instance.checkPeopleChanged());
unawaited(syncSmartAlbums());
unawaited(SmartAlbumsService.instance.syncSmartAlbums());
},
);
},

View File

@@ -128,4 +128,21 @@ extension EntitiesDB on FilesDB {
}
return maps.values.first as String?;
}
Future<Map<String, int>> getUpdatedAts(
EntityType type,
List<String> ids,
) async {
final db = await sqliteAsyncDB;
final List<Map<String, dynamic>> maps = await db.getAll(
'SELECT id, updatedAt FROM entities WHERE type = ? AND id IN (${List.filled(ids.length, '?').join(',')})',
[type.name, ...ids],
);
return Map<String, int>.fromEntries(
List.generate(
maps.length,
(i) => MapEntry(maps[i]['id'] as String, maps[i]['updatedAt'] as int),
),
);
}
}

View File

@@ -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<SmartAlbumConfig> getUpdatedConfig(Set<String> newPersonsIds) async {
final toAdd = newPersonsIds.difference(personIDs);
final toRemove = personIDs.difference(newPersonsIds);
@@ -65,101 +53,4 @@ class SmartAlbumConfig {
addedFiles: newFiles,
);
}
Future<void> 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<SmartAlbumConfig> loadConfig(int collectionId) async {
final personIDs =
prefs.getStringList("${_personIdsKey}_$collectionId") ?? [];
final addedFilesString =
prefs.getString("${_addedFilesKey}_$collectionId") ?? "";
final addedFiles = <String, (int, Set<int>)>{};
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<void> 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();
}
}
}
}
});
}

View File

@@ -45,10 +45,6 @@ class ServiceLocator {
}
}
SharedPreferences get prefs {
return ServiceLocator.instance.prefs;
}
FlagService? _flagService;
FlagService get flagService {

View File

@@ -249,4 +249,11 @@ class EntityService {
final hash = md5.convert(utf8.encode(preHash)).toString().substring(0, 10);
return hash;
}
Future<Map<String, int>> getUpdatedAts(
EntityType type,
List<String> personIds,
) async {
return await _db.getUpdatedAts(type, personIds);
}
}

View File

@@ -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<int, SmartAlbumConfig> _cachedConfigs = {};
bool isInitialized = false;
void init() {
_logger.info("SmartAlbumsService initialized");
refresh().ignore();
}
Future<void> 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<void> 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<void> 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<SmartAlbumConfig> getConfig(int collectionId) async {
if (isInitialized && _cachedConfigs.containsKey(collectionId)) {
return _cachedConfigs[collectionId]!;
}
refresh().ignore();
return await loadConfig(collectionId);
}
Future<SmartAlbumConfig> 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 = <String, (int, Set<int>)>{};
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,
);
}
}

View File

@@ -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();
}
}

View File

@@ -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<SmartAlbumPeople> {
final _selectedPeople = SelectedPeople();
late SmartAlbumConfig currentConfig;
SmartAlbumConfig? currentConfig;
bool isLoading = false;
@override
@@ -36,9 +37,12 @@ class _SmartAlbumPeopleState extends State<SmartAlbumPeople> {
}
Future<void> 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<SmartAlbumPeople> {
: () 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 = <String, (int, Set<int>)>{};
// 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;