import "package:flutter/material.dart"; import "package:logging/logging.dart"; import "package:photos/core/configuration.dart"; import "package:photos/core/constants.dart"; import "package:photos/db/files_db.dart"; import "package:photos/db/ml/db.dart"; import "package:photos/generated/l10n.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/file/file_type.dart"; import "package:photos/models/location_tag/location_tag.dart"; import "package:photos/models/ml/face/person.dart"; import "package:photos/models/search/hierarchical/album_filter.dart"; import "package:photos/models/search/hierarchical/contacts_filter.dart"; import "package:photos/models/search/hierarchical/face_filter.dart"; import "package:photos/models/search/hierarchical/file_type_filter.dart"; import "package:photos/models/search/hierarchical/hierarchical_search_filter.dart"; import "package:photos/models/search/hierarchical/location_filter.dart"; import "package:photos/models/search/hierarchical/magic_filter.dart"; import "package:photos/models/search/hierarchical/only_them_filter.dart"; import "package:photos/models/search/hierarchical/top_level_generic_filter.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/collections_service.dart"; import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; import "package:photos/services/machine_learning/face_ml/person/person_service.dart"; import "package:photos/services/magic_cache_service.dart"; import "package:photos/services/search_service.dart"; import "package:photos/ui/viewer/gallery/state/search_filter_data_provider.dart"; import "package:photos/utils/file_util.dart"; Future> getFilteredFiles( List filters, ) async { final logger = Logger("HierarchicalSearchUtil"); final mlDataDB = MLDataDB.instance; late final List filteredFiles; final files = await SearchService.instance.getAllFilesForHierarchicalSearch(); final resultsNeverComputedFilters = []; final ignoredCollections = CollectionsService.instance.getHiddenCollectionIds(); logger.info("Getting filtered files for Filters: $filters"); for (HierarchicalSearchFilter filter in filters) { if (filter is FaceFilter && filter.matchedUploadedIDs.isEmpty) { try { if (filter.personId != null) { final fileIDs = await mlDataDB.getFileIDsOfPersonID( filter.personId!, ); filter.matchedUploadedIDs.addAll(fileIDs); } else if (filter.clusterId != null) { final fileIDs = await mlDataDB.getFileIDsOfClusterID( filter.clusterId!, ); filter.matchedUploadedIDs.addAll(fileIDs); } } catch (e) { logger.severe("Error in filtering face filter: $e"); } } else if (filter is OnlyThemFilter && filter.matchedUploadedIDs.isEmpty) { try { late Set intersectionOfSelectedFaceFiltersFileIDs; final selectedClusterIDs = []; final selectedPersonIDs = []; int index = 0; for (final faceFilter in filter.faceFilters) { if (index == 0) { intersectionOfSelectedFaceFiltersFileIDs = faceFilter.matchedUploadedIDs; } else { intersectionOfSelectedFaceFiltersFileIDs = intersectionOfSelectedFaceFiltersFileIDs .intersection(faceFilter.matchedUploadedIDs); } index++; if (faceFilter.clusterId != null) { selectedClusterIDs.add(faceFilter.clusterId!); } else { selectedPersonIDs.add(faceFilter.personId!); } } await mlDataDB .getPersonsClusterIDs(selectedPersonIDs) .then((clusterIDs) { selectedClusterIDs.addAll(clusterIDs); }); final fileIDsToAvoid = await mlDataDB.getAllFilesAssociatedWithAllClusters( exceptClusters: selectedClusterIDs, ); final filesOfFaceIDsNotInAnyCluster = await mlDataDB.getAllFileIDsOfFaceIDsNotInAnyCluster(); fileIDsToAvoid.addAll(filesOfFaceIDsNotInAnyCluster); final result = intersectionOfSelectedFaceFiltersFileIDs.difference(fileIDsToAvoid); filter.matchedUploadedIDs.addAll(result); } catch (e) { logger.severe("Error in filtering only them filter: $e"); } } else if (filter.matchedUploadedIDs.isEmpty) { resultsNeverComputedFilters.add(filter); } } try { for (EnteFile file in files) { if (file.uploadedFileID == null || file.uploadedFileID == -1) { continue; } for (HierarchicalSearchFilter filter in resultsNeverComputedFilters) { if (filter.isMatch(file)) { filter.matchedUploadedIDs.add(file.uploadedFileID!); } } } Set filteredUploadedIDs = {}; for (int i = 0; i < filters.length; i++) { if (i == 0) { filteredUploadedIDs = filteredUploadedIDs.union(filters[i].matchedUploadedIDs); } else { filteredUploadedIDs = filteredUploadedIDs.intersection(filters[i].matchedUploadedIDs); } } filteredFiles = await FilesDB.instance.getFilesFromIDs( filteredUploadedIDs.toList(), dedupeByUploadId: true, collectionsToIgnore: ignoredCollections, ); } catch (e) { Logger("HierarchicalSearchUtil").severe("Failed to get filtered files: $e"); } return filteredFiles; } Future curateFilters( SearchFilterDataProvider searchFilterDataProvider, List files, BuildContext context, ) async { try { final albumFilters = await _curateAlbumFilters(files); final fileTypeFilters = _curateFileTypeFilters(files, context); final locationFilters = await _curateLocationFilters( files, ); final contactsFilters = _curateContactsFilter(files); final faceFilters = await curateFaceFilters(files); final magicFilters = await curateMagicFilters(files, context); final onlyThemFilter = getOnlyThemFilter( searchFilterDataProvider, context, ); searchFilterDataProvider.clearAndAddRecommendations( [ ...onlyThemFilter, ...magicFilters, ...faceFilters, ...fileTypeFilters, ...contactsFilters, ...albumFilters, ...locationFilters, ], ); } catch (e) { Logger("HierarchicalSearchUtil").severe("Failed to curate filters", e); } } List getOnlyThemFilter( SearchFilterDataProvider searchFilterDataProvider, BuildContext context, ) { if (searchFilterDataProvider.initialGalleryFilter is FaceFilter && searchFilterDataProvider.appliedFilters.isEmpty) { return [ OnlyThemFilter( faceFilters: [ searchFilterDataProvider.initialGalleryFilter as FaceFilter, ], onlyThemString: S.of(context).onlyThem, occurrence: kMostRelevantFilter, ), ]; } final appliedFaceFilters = searchFilterDataProvider.appliedFilters.whereType().toList(); if (appliedFaceFilters.isEmpty || appliedFaceFilters.length > 4) { return []; } else { final onlyThemFilter = OnlyThemFilter( faceFilters: appliedFaceFilters, onlyThemString: S.of(context).onlyThem, occurrence: kMostRelevantFilter, ); return [onlyThemFilter]; } } Future> _curateAlbumFilters( List files, ) async { final albumFilters = []; final idToOccurrence = {}; final uploadedIDs = []; for (EnteFile file in files) { if (file.uploadedFileID != null && file.uploadedFileID != -1) { uploadedIDs.add(file.uploadedFileID!); } } final collectionIDsOfFiles = await FilesDB.instance.getAllCollectionIDsOfFiles(uploadedIDs); for (int collectionID in collectionIDsOfFiles) { idToOccurrence[collectionID] = (idToOccurrence[collectionID] ?? 0) + 1; } for (int id in idToOccurrence.keys) { final collection = CollectionsService.instance.getCollectionByID(id); if (collection == null) { continue; } albumFilters.add( AlbumFilter( collectionID: id, albumName: collection.displayName, occurrence: idToOccurrence[id]!, ), ); } return albumFilters; } List _curateFileTypeFilters( List files, BuildContext context, ) { final fileTypeFilters = []; int photosCount = 0; int videosCount = 0; int livePhotosCount = 0; for (EnteFile file in files) { final id = file.uploadedFileID; if (id != null && id != -1) { if (file.fileType == FileType.image) { photosCount++; } else if (file.fileType == FileType.video) { videosCount++; } else if (file.fileType == FileType.livePhoto) { livePhotosCount++; } } } if (photosCount > 0) { fileTypeFilters.add( FileTypeFilter( fileType: FileType.image, typeName: S.of(context).photos, occurrence: photosCount, ), ); } if (videosCount > 0) { fileTypeFilters.add( FileTypeFilter( fileType: FileType.video, typeName: S.of(context).videos, occurrence: videosCount, ), ); } if (livePhotosCount > 0) { fileTypeFilters.add( FileTypeFilter( fileType: FileType.livePhoto, typeName: S.of(context).livePhotos, occurrence: livePhotosCount, ), ); } return fileTypeFilters; } Future> _curateLocationFilters( List files, ) async { final locationFilters = []; final locationTagToOccurrence = await locationService.getLocationTagsToOccurance(files); for (LocationTag locationTag in locationTagToOccurrence.keys) { locationFilters.add( LocationFilter( locationTag: locationTag, occurrence: locationTagToOccurrence[locationTag]!, ), ); } return locationFilters; } List _curateContactsFilter( List files, ) { final contactsFilters = []; final ownerIdToOccurrence = {}; for (EnteFile file in files) { if (file.ownerID == Configuration.instance.getUserID() || file.uploadedFileID == null || file.uploadedFileID == -1 || file.ownerID == null) { continue; } ownerIdToOccurrence[file.ownerID!] = (ownerIdToOccurrence[file.ownerID] ?? 0) + 1; } for (int id in ownerIdToOccurrence.keys) { final user = CollectionsService.instance.getFileOwner(id, null); contactsFilters.add( ContactsFilter( user: user, occurrence: ownerIdToOccurrence[id]!, ), ); } return contactsFilters; } Future> curateFaceFilters( List files, ) async { try { final mlDataDB = MLDataDB.instance; final faceFilters = []; final Map> fileIdToClusterID = await mlDataDB.getFileIdToClusterIds(); final Map personIdToPerson = await PersonService.instance.getPersonsMap(); final clusterIDToPersonID = await mlDataDB.getClusterIDToPersonID(); final Map> clusterIdToFiles = {}; final Map> personIdToFiles = {}; for (final f in files) { if (!fileIdToClusterID.containsKey(f.uploadedFileID ?? -1)) { continue; } final clusterIds = fileIdToClusterID[f.uploadedFileID ?? -1]!; for (final clusterId in clusterIds) { final PersonEntity? p = personIdToPerson[clusterIDToPersonID[clusterId] ?? ""]; if (p != null) { if (personIdToFiles.containsKey(p.remoteID)) { personIdToFiles[p.remoteID]!.add(f); } else { personIdToFiles[p.remoteID] = [f]; } } else { if (clusterIdToFiles.containsKey(clusterId)) { clusterIdToFiles[clusterId]!.add(f); } else { clusterIdToFiles[clusterId] = [f]; } } } } for (final personID in personIdToFiles.keys) { final files = personIdToFiles[personID]!; if (files.isEmpty) { continue; } final PersonEntity p = personIdToPerson[personID]!; if (p.data.isIgnored) continue; faceFilters.add( FaceFilter( personId: personID, clusterId: null, faceName: p.data.name, faceFile: files.first, occurrence: files.length, ), ); } for (final clusterId in clusterIdToFiles.keys) { final files = clusterIdToFiles[clusterId]!; if (clusterIDToPersonID[clusterId] != null) { // This should not happen, means a faceID is assigned to multiple persons. Logger("hierarchical_search_util").severe( "`getAllFace`: Cluster $clusterId should not have person id ${clusterIDToPersonID[clusterId]}", ); } if (files.length < kMinimumClusterSizeSearchResult) continue; faceFilters.add( FaceFilter( personId: null, clusterId: clusterId, faceName: null, faceFile: files.first, occurrence: files.length, ), ); } return faceFilters; } catch (e, s) { Logger("hierarchical_search_util") .severe("Error in curating face filters", e, s); rethrow; } } Future> curateMagicFilters( List files, BuildContext context, ) async { final magicFilters = []; final magicCaches = await magicCacheService.getMagicCache(); final filesUploadedFileIDs = filesToUploadedFileIDs(files); for (MagicCache magicCache in magicCaches) { final uploadedIDs = magicCache.fileUploadedIDs.toSet(); final intersection = uploadedIDs.intersection(filesUploadedFileIDs); final title = getLocalizedTitle(context, magicCache.title); if (intersection.length > 3) { magicFilters.add( MagicFilter( filterName: title, occurrence: intersection.length, matchedUploadedIDs: magicCache.fileUploadedIDs.toSet(), ), ); } } return magicFilters; } Map> getFiltersForBottomSheet( SearchFilterDataProvider searchFilterDataProvider, ) { final onlyThemFilter = searchFilterDataProvider.appliedFilters .whereType() .toList(); onlyThemFilter.addAll( searchFilterDataProvider.recommendations.whereType(), ); final faceFilters = searchFilterDataProvider.appliedFilters.whereType().toList(); faceFilters .addAll(searchFilterDataProvider.recommendations.whereType()); final albumFilters = searchFilterDataProvider.appliedFilters.whereType().toList(); albumFilters.addAll( searchFilterDataProvider.recommendations.whereType(), ); final fileTypeFilters = searchFilterDataProvider.appliedFilters .whereType() .toList(); fileTypeFilters.addAll( searchFilterDataProvider.recommendations.whereType(), ); final locationFilters = searchFilterDataProvider.appliedFilters .whereType() .toList(); locationFilters.addAll( searchFilterDataProvider.recommendations.whereType(), ); final contactsFilters = searchFilterDataProvider.appliedFilters .whereType() .toList(); contactsFilters.addAll( searchFilterDataProvider.recommendations.whereType(), ); final magicFilters = searchFilterDataProvider.appliedFilters.whereType().toList(); magicFilters.addAll( searchFilterDataProvider.recommendations.whereType(), ); final topLevelGenericFilter = searchFilterDataProvider.appliedFilters .whereType() .toList(); return { "onlyThemFilter": onlyThemFilter, "faceFilters": faceFilters, "magicFilters": magicFilters, "locationFilters": locationFilters, "contactsFilters": contactsFilters, "albumFilters": albumFilters, "fileTypeFilters": fileTypeFilters, "topLevelGenericFilter": topLevelGenericFilter, }; } List getRecommendedFiltersForAppBar( SearchFilterDataProvider searchFilterDataProvider, ) { final recommendations = searchFilterDataProvider.recommendations; final mostRelevantFilterFromEachType = []; int index = 0; final totalRecommendations = recommendations.length; // Add the most relevant filter from each type available in the first half of // the recommendations list for (final filter in recommendations) { if (mostRelevantFilterFromEachType .every((element) => element.runtimeType != filter.runtimeType)) { mostRelevantFilterFromEachType.add(filter); } if (mostRelevantFilterFromEachType.length == (FilterTypeNames.values.length) || (index + 1) / totalRecommendations > 0.5) { break; } index++; } final curatedRecommendations = [ ...mostRelevantFilterFromEachType, ]; for (HierarchicalSearchFilter recommendation in recommendations) { if (curatedRecommendations.length >= kMaxAppbarFilters) { break; } if (mostRelevantFilterFromEachType.every( (element) => !element.isSameFilter(recommendation), )) { curatedRecommendations.add(recommendation); } } final faceReccos = []; final magicReccos = []; final locationReccos = []; final contactsReccos = []; final albumReccos = []; final fileTypeReccos = []; final onlyThemFilter = []; for (var recommendation in curatedRecommendations) { if (recommendation is OnlyThemFilter) { onlyThemFilter.add(recommendation); } else if (recommendation is FaceFilter) { faceReccos.add(recommendation); } else if (recommendation is MagicFilter) { magicReccos.add(recommendation); } else if (recommendation is LocationFilter) { locationReccos.add(recommendation); } else if (recommendation is ContactsFilter) { contactsReccos.add(recommendation); } else if (recommendation is AlbumFilter) { albumReccos.add(recommendation); } else if (recommendation is FileTypeFilter) { fileTypeReccos.add(recommendation); } } return [ ...onlyThemFilter, ...faceReccos, ...magicReccos, ...locationReccos, ...contactsReccos, ...albumReccos, ...fileTypeReccos, ]; }