From 49fe5f41e009b10f44944dfaac3ef2d9ed080f5c Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Mar 2025 12:42:49 +0530 Subject: [PATCH 01/19] [mob][photos] easier debugging --- mobile/lib/models/search/search_types.dart | 1 + mobile/lib/services/search_service.dart | 8 +++++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/mobile/lib/models/search/search_types.dart b/mobile/lib/models/search/search_types.dart index 63d1543042..e3e5e19136 100644 --- a/mobile/lib/models/search/search_types.dart +++ b/mobile/lib/models/search/search_types.dart @@ -242,6 +242,7 @@ extension SectionTypeExtensions on SectionType { case SectionType.moment: if (flagService.internalUser) { + // TODO: lau: remove this whole smart memories and moment altogether return SearchService.instance.smartMemories(context, limit); } return SearchService.instance.getRandomMomentsSearchResults(context); diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index c25dda586b..8a3880f9a9 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1193,7 +1193,13 @@ class SearchService { BuildContext context, int? limit, ) async { - final memories = await memoriesCacheService.getMemories(limit); + final fakeCache = MemoriesCache( + toShowMemories: [], + peopleShownLogs: [], + tripsShownLogs: [], + ); + final memories = + await smartMemoriesService.calcMemories(DateTime.now(), fakeCache); final searchResults = []; for (final memory in memories) { final files = Memory.filesFromMemories(memory.memories); From 2d30ac4c46521fe836dbf1d2ad0f5e74d24990be Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Mar 2025 12:43:40 +0530 Subject: [PATCH 02/19] [mob][photos] include import --- mobile/lib/services/search_service.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 8a3880f9a9..ce67a5c229 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -23,6 +23,7 @@ import 'package:photos/models/file/file_type.dart'; import "package:photos/models/local_entity_data.dart"; import "package:photos/models/location/location.dart"; import "package:photos/models/location_tag/location_tag.dart"; +import "package:photos/models/memories/memories_cache.dart"; import "package:photos/models/memories/memory.dart"; import "package:photos/models/ml/face/person.dart"; import 'package:photos/models/search/album_search_result.dart'; From d25e81282de70173ed2611e8f34933edd46ee1a5 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Mar 2025 13:13:22 +0530 Subject: [PATCH 03/19] [mob][photos] Mini refactor --- .../lib/services/smart_memories_service.dart | 78 +++++++++---------- 1 file changed, 39 insertions(+), 39 deletions(-) diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index 2afe4190aa..2d5d5b4230 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -496,12 +496,13 @@ class SmartMemoriesService { final List<(List, Location)> smallRadiusClusters = []; final List<(List, Location)> wideRadiusClusters = []; // Go through all files and cluster the ones not inside any location tag + allFilesLoop: for (EnteFile file in allFiles) { if (!file.hasLocation || file.uploadedFileID == null || !file.isOwner || file.creationTime == null) { - continue; + continue allFilesLoop; } // Check if the file is inside any location tag bool hasLocationTag = false; @@ -516,42 +517,41 @@ class SmartMemoriesService { } } // Cluster the files not inside any location tag (incremental clustering) - if (!hasLocationTag) { - // Small radius clustering for base locations - bool foundSmallCluster = false; - for (final cluster in smallRadiusClusters) { - final clusterLocation = cluster.$2; - if (isFileInsideLocationTag( - clusterLocation, - file.location!, - 0.6, - )) { - cluster.$1.add(file); - foundSmallCluster = true; - break; - } + if (hasLocationTag) continue allFilesLoop; + // Small radius clustering for base locations + bool addedToExistingSmallCluster = false; + for (final cluster in smallRadiusClusters) { + final clusterLocation = cluster.$2; + if (isFileInsideLocationTag( + clusterLocation, + file.location!, + 0.6, + )) { + cluster.$1.add(file); + addedToExistingSmallCluster = true; + break; } - if (!foundSmallCluster) { - smallRadiusClusters.add(([file], file.location!)); - } - // Wide radius clustering for trip locations - bool foundWideCluster = false; - for (final cluster in wideRadiusClusters) { - final clusterLocation = cluster.$2; - if (isFileInsideLocationTag( - clusterLocation, - file.location!, - 100.0, - )) { - cluster.$1.add(file); - foundWideCluster = true; - break; - } - } - if (!foundWideCluster) { - wideRadiusClusters.add(([file], file.location!)); + } + if (!addedToExistingSmallCluster) { + smallRadiusClusters.add(([file], file.location!)); + } + // Wide radius clustering for trip locations + bool addedToExistingWideCluster = false; + for (final cluster in wideRadiusClusters) { + final clusterLocation = cluster.$2; + if (isFileInsideLocationTag( + clusterLocation, + file.location!, + 100.0, + )) { + cluster.$1.add(file); + addedToExistingWideCluster = true; + break; } } + if (!addedToExistingWideCluster) { + wideRadiusClusters.add(([file], file.location!)); + } } // Identify base locations @@ -577,11 +577,11 @@ class SmartMemoriesService { final lastCreationTime = DateTime.fromMicrosecondsSinceEpoch( creationTimes.last, ); - if (lastCreationTime.difference(firstCreationTime).inDays < 90) { + final daysRange = lastCreationTime.difference(firstCreationTime).inDays; + if (daysRange < 90) { continue; } // Check for a minimum average number of days photos are clicked in range - final daysRange = lastCreationTime.difference(firstCreationTime).inDays; if (uniqueDays.length < daysRange * 0.1) continue; // Check if it's a current or old base location final bool isCurrent = lastCreationTime.isAfter( @@ -1354,9 +1354,9 @@ class SmartMemoriesService { final fileToScore = await MLComputer.instance .compareEmbeddings(vectors, _clipPositiveTextVector!); final fileIdToClip = {}; - for (final vector in vectors) { - fileIdToClip[vector.fileID] = vector; - } + for (final vector in vectors) { + fileIdToClip[vector.fileID] = vector; + } // Get face scores for each file final fileToFaceCount = {}; From c43212511388ad28271d31de66de66fba14f3bbd Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Mar 2025 16:41:12 +0530 Subject: [PATCH 04/19] [mob][photos] Make base locations more robust --- mobile/lib/services/smart_memories_service.dart | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index 2d5d5b4230..d162a06e3a 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -583,6 +583,14 @@ class SmartMemoriesService { } // Check for a minimum average number of days photos are clicked in range if (uniqueDays.length < daysRange * 0.1) continue; + // Check that there isn't a huge time gap somewhere in the range + final int gapThreshold = (daysRange * 0.6).round() * microSecondsInDay; + int maxGap = 0; + for (int i = 1; i < creationTimes.length; i++) { + final gap = creationTimes[i] - creationTimes[i - 1]; + if (gap > maxGap) maxGap = gap; + } + if (maxGap > gapThreshold) continue; // Check if it's a current or old base location final bool isCurrent = lastCreationTime.isAfter( DateTime.now().subtract( From d49f9cc054196aabcb1b3afa5820ce513b2793af Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Mar 2025 17:12:46 +0530 Subject: [PATCH 05/19] [mob][photos] Use constants --- mobile/lib/services/smart_memories_service.dart | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index d162a06e3a..cb3e310fe1 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -489,6 +489,10 @@ class SmartMemoriesService { final currentMonth = currentTime.month; final cutOffTime = currentTime.subtract(const Duration(days: 365)); + const baseRadius = 0.6; + const tripRadius = 100.0; + const overlapRadius = 10.0; + final Map, List> tagToItemsMap = {}; for (int i = 0; i < locationTagEntities.length; i++) { tagToItemsMap[locationTagEntities.elementAt(i)] = []; @@ -525,7 +529,7 @@ class SmartMemoriesService { if (isFileInsideLocationTag( clusterLocation, file.location!, - 0.6, + baseRadius, )) { cluster.$1.add(file); addedToExistingSmallCluster = true; @@ -542,7 +546,7 @@ class SmartMemoriesService { if (isFileInsideLocationTag( clusterLocation, file.location!, - 100.0, + tripRadius, )) { cluster.$1.add(file); addedToExistingWideCluster = true; @@ -612,7 +616,7 @@ class SmartMemoriesService { if (isFileInsideLocationTag( baseLocation.location, location, - 10.0, + overlapRadius, )) { tooClose = true; break; @@ -622,7 +626,7 @@ class SmartMemoriesService { if (isFileInsideLocationTag( tag.item.centerPoint, location, - 10.0, + overlapRadius, )) { tooClose = true; break; From 5ff494320cbcca6a5f8c18a8e7ff6f9f17735ca1 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 5 Mar 2025 17:43:51 +0530 Subject: [PATCH 06/19] [mob][photos] trips dont repeat early --- .../lib/services/smart_memories_service.dart | 65 +++++++++++-------- 1 file changed, 39 insertions(+), 26 deletions(-) diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index cb3e310fe1..8a6ce2a7c0 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -94,7 +94,8 @@ class SmartMemoriesService { _logger.finest("All files length: ${allFiles.length}"); // Trip memories - final tripMemories = await _getTripsResults(allFiles, now); + final tripMemories = + await _getTripsResults(allFiles, now, oldCache.tripsShownLogs); _deductUsedMemories(allFiles, tripMemories); memories.addAll(tripMemories); _logger.finest("All files length: ${allFiles.length}"); @@ -478,6 +479,7 @@ class SmartMemoriesService { Future> _getTripsResults( Iterable allFiles, DateTime currentTime, + List shownTrips, ) async { final List memoryResults = []; final Iterable> locationTagEntities = @@ -856,17 +858,14 @@ class SmartMemoriesService { // Otherwise, if no trips happened in the current month, // look for the earliest upcoming trip in another month that has 3+ trips. else { - // TODO lau: make sure the same upcoming trip isn't shown multiple times over multiple months final sortedUpcomingMonths = - List.generate(12, (i) => ((currentMonth + i) % 12) + 1); + List.generate(6, (i) => ((currentMonth + i) % 12) + 1); checkUpcomingMonths: for (final month in sortedUpcomingMonths) { if (tripsByMonthYear.containsKey(month)) { final List thatMonthTrips = []; for (final trips in tripsByMonthYear[month]!.values) { - for (final trip in trips) { - thatMonthTrips.add(trip); - } + thatMonthTrips.addAll(trips); } if (thatMonthTrips.length >= 3) { // take and use the third earliest trip @@ -874,27 +873,41 @@ class SmartMemoriesService { (a, b) => a.averageCreationTime().compareTo(b.averageCreationTime()), ); - final trip = thatMonthTrips[2]; - final year = - DateTime.fromMicrosecondsSinceEpoch(trip.averageCreationTime()) - .year; - final String? locationName = _tryFindLocationName(trip.memories); - String name = "Trip in $year"; - if (locationName != null) { - name = "Trip to $locationName"; - } else if (year == currentTime.year - 1) { - name = "Last year's trip"; + checkPotentialTrips: + for (final trip in thatMonthTrips.sublist(2)) { + for (final shownTrip in shownTrips) { + final distance = + calculateDistance(trip.location, shownTrip.location); + final shownTripDate = DateTime.fromMicrosecondsSinceEpoch( + shownTrip.lastTimeShown, + ); + final shownRecently = + currentTime.difference(shownTripDate) < kTripShowTimeout; + if (distance < overlapRadius && shownRecently) { + continue checkPotentialTrips; + } + } + final year = DateTime.fromMicrosecondsSinceEpoch( + trip.averageCreationTime(), + ).year; + final String? locationName = _tryFindLocationName(trip.memories); + String name = "Trip in $year"; + if (locationName != null) { + name = "Trip to $locationName"; + } else if (year == currentTime.year - 1) { + name = "Last year's trip"; + } + final photoSelection = await _bestSelection(trip.memories); + memoryResults.add( + trip.copyWith( + memories: photoSelection, + title: name, + firstDateToShow: nowInMicroseconds, + lastDateToShow: windowEnd, + ), + ); + break checkUpcomingMonths; } - final photoSelection = await _bestSelection(trip.memories); - memoryResults.add( - trip.copyWith( - memories: photoSelection, - title: name, - firstDateToShow: nowInMicroseconds, - lastDateToShow: windowEnd, - ), - ); - break checkUpcomingMonths; } } } From e2aea632761fc816b0c01c6720784b7848b8791b Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Thu, 6 Mar 2025 16:33:41 +0530 Subject: [PATCH 07/19] [mob][photos] base locations in locations section --- mobile/lib/models/base_location.dart | 52 +++++++++++++++ .../lib/models/memories/memories_cache.dart | 23 ++++++- mobile/lib/services/location_service.dart | 3 + .../lib/services/memories_cache_service.dart | 36 ++++++++--- mobile/lib/services/search_service.dart | 51 ++++++++++++--- .../lib/services/smart_memories_service.dart | 63 ++++++++++--------- 6 files changed, 179 insertions(+), 49 deletions(-) diff --git a/mobile/lib/models/base_location.dart b/mobile/lib/models/base_location.dart index 38b7847c64..b7834fd089 100644 --- a/mobile/lib/models/base_location.dart +++ b/mobile/lib/models/base_location.dart @@ -1,6 +1,10 @@ +import "dart:convert"; + import "package:photos/models/file/file.dart"; import "package:photos/models/location/location.dart"; +const baseRadius = 0.6; + class BaseLocation { final List files; int? firstCreationTime; @@ -16,6 +20,54 @@ class BaseLocation { this.lastCreationTime, }); + static List decodeJsonToList( + String jsonString, + Map filesMap, + ) { + final jsonList = jsonDecode(jsonString) as List; + return jsonList + .map((json) => BaseLocation.fromJson(json, filesMap)) + .toList(); + } + + static String encodeListToJson(List baseLocations) { + final jsonList = + baseLocations.map((location) => location.toJson()).toList(); + return jsonEncode(jsonList); + } + + static BaseLocation fromJson( + Map json, + Map filesMap, + ) { + return BaseLocation( + (json['fileIDs'] as List).map((e) => filesMap[e]!).toList(), + Location( + latitude: json['location']['latitude'], + longitude: json['location']['longitude'], + ), + json['isCurrentBase'] as bool, + firstCreationTime: json['firstCreationTime'] as int?, + lastCreationTime: json['lastCreationTime'] as int?, + ); + } + + Map toJson() { + return { + 'fileIDs': files + .where((file) => file.uploadedFileID != null) + .map((file) => file.uploadedFileID!) + .toList(), + 'location': { + 'latitude': location.latitude!, + 'longitude': location.longitude!, + }, + 'isCurrentBase': isCurrentBase, + 'firstCreationTime': firstCreationTime, + 'lastCreationTime': lastCreationTime, + }; + } + int averageCreationTime() { if (firstCreationTime != null && lastCreationTime != null) { return (firstCreationTime! + lastCreationTime!) ~/ 2; diff --git a/mobile/lib/models/memories/memories_cache.dart b/mobile/lib/models/memories/memories_cache.dart index 5298c7f81a..06a57341d9 100644 --- a/mobile/lib/models/memories/memories_cache.dart +++ b/mobile/lib/models/memories/memories_cache.dart @@ -1,5 +1,7 @@ import "dart:convert"; +import "package:photos/models/base_location.dart"; +import "package:photos/models/file/file.dart"; import "package:photos/models/location/location.dart"; import "package:photos/models/memories/people_memory.dart"; import "package:photos/models/memories/smart_memory.dart"; @@ -20,18 +22,29 @@ class MemoriesCache { final List toShowMemories; final List peopleShownLogs; final List tripsShownLogs; + final List baseLocations; MemoriesCache({ required this.toShowMemories, required this.peopleShownLogs, required this.tripsShownLogs, + required this.baseLocations, }); - factory MemoriesCache.fromJson(Map json) { + factory MemoriesCache.fromJson( + Map json, + Map filesMap, + ) { return MemoriesCache( toShowMemories: ToShowMemory.decodeJsonToList(json['toShowMemories']), peopleShownLogs: PeopleShownLog.decodeJsonToList(json['peopleShownLogs']), tripsShownLogs: TripsShownLog.decodeJsonToList(json['tripsShownLogs']), + baseLocations: json['baseLocations'] != null + ? BaseLocation.decodeJsonToList( + json['baseLocations'], + filesMap, + ) + : [], ); } @@ -40,6 +53,7 @@ class MemoriesCache { 'toShowMemories': ToShowMemory.encodeListToJson(toShowMemories), 'peopleShownLogs': PeopleShownLog.encodeListToJson(peopleShownLogs), 'tripsShownLogs': TripsShownLog.encodeListToJson(tripsShownLogs), + 'baseLocations': BaseLocation.encodeListToJson(baseLocations), }; } @@ -47,8 +61,11 @@ class MemoriesCache { return jsonEncode(cache.toJson()); } - static MemoriesCache decodeFromJsonString(String jsonString) { - return MemoriesCache.fromJson(jsonDecode(jsonString)); + static MemoriesCache decodeFromJsonString( + String jsonString, + Map filesMap, + ) { + return MemoriesCache.fromJson(jsonDecode(jsonString), filesMap); } } diff --git a/mobile/lib/services/location_service.dart b/mobile/lib/services/location_service.dart index cd00ac4c85..f0b20a44a4 100644 --- a/mobile/lib/services/location_service.dart +++ b/mobile/lib/services/location_service.dart @@ -10,6 +10,7 @@ import "package:photos/core/event_bus.dart"; import "package:photos/events/location_tag_updated_event.dart"; import "package:photos/extensions/stop_watch.dart"; import "package:photos/models/api/entity/type.dart"; +import "package:photos/models/base_location.dart"; import "package:photos/models/file/file.dart"; import "package:photos/models/local_entity_data.dart"; import "package:photos/models/location/location.dart"; @@ -33,6 +34,8 @@ class LocationService { List _cities = []; + List baseLocations = []; + LocationService(this.prefs) { debugPrint('LocationService constructor'); Future.delayed(const Duration(seconds: 3), () { diff --git a/mobile/lib/services/memories_cache_service.dart b/mobile/lib/services/memories_cache_service.dart index fdbb362d3f..649204e719 100644 --- a/mobile/lib/services/memories_cache_service.dart +++ b/mobile/lib/services/memories_cache_service.dart @@ -140,26 +140,27 @@ class MemoriesCacheService { // calculate memories for this period and for the next period final now = DateTime.now(); final next = now.add(kMemoriesUpdateFrequency); - final nowMemories = - await smartMemoriesService.calcMemories(now, newCache); - final nextMemories = + final nowResult = await smartMemoriesService.calcMemories(now, newCache); + final nextResult = await smartMemoriesService.calcMemories(next, newCache); w?.log("calculated new memories"); - for (final nowMemory in nowMemories) { + for (final nowMemory in nowResult.memories) { newCache.toShowMemories .add(ToShowMemory.fromSmartMemory(nowMemory, now)); } - for (final nextMemory in nextMemories) { + for (final nextMemory in nextResult.memories) { newCache.toShowMemories .add(ToShowMemory.fromSmartMemory(nextMemory, next)); } + newCache.baseLocations.addAll(nowResult.baseLocations); w?.log("added memories to cache"); final file = File(await _getCachePath()); if (!file.existsSync()) { file.createSync(recursive: true); } _cachedMemories = - nowMemories.where((memory) => memory.shouldShowNow()).toList(); + nowResult.memories.where((memory) => memory.shouldShowNow()).toList(); + locationService.baseLocations = nowResult.baseLocations; await file.writeAsBytes( MemoriesCache.encodeToJsonString(newCache).codeUnits, ); @@ -174,8 +175,14 @@ class MemoriesCacheService { } } + /// WARNING: Use for testing only, TODO: lau: remove later + Future debugCacheForTesting() async { + final oldCache = await _readCacheFromDisk(); + final MemoriesCache newCache = _processOldCache(oldCache); + return newCache; + } + MemoriesCache _processOldCache(MemoriesCache? oldCache) { - final List toShowMemories = []; final List peopleShownLogs = []; final List tripsShownLogs = []; if (oldCache != null) { @@ -221,9 +228,10 @@ class MemoriesCacheService { } } return MemoriesCache( - toShowMemories: toShowMemories, + toShowMemories: [], peopleShownLogs: peopleShownLogs, tripsShownLogs: tripsShownLogs, + baseLocations: [], ); } @@ -259,6 +267,7 @@ class MemoriesCacheService { ); } } + locationService.baseLocations = cache.baseLocations; _logger.info('Processing of disk cache memories done'); return memories; } catch (e, s) { @@ -294,8 +303,17 @@ class MemoriesCacheService { _logger.info("No memories cache found"); return null; } + final allFiles = Set.from( + await SearchService.instance.getAllFilesForSearch(), + ); + final allFileIdsToFile = {}; + for (final file in allFiles) { + if (file.uploadedFileID != null) { + allFileIdsToFile[file.uploadedFileID!] = file; + } + } final jsonString = file.readAsStringSync(); - return MemoriesCache.decodeFromJsonString(jsonString); + return MemoriesCache.decodeFromJsonString(jsonString, allFileIdsToFile); } Future clearMemoriesCache() async { diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index ce67a5c229..5e47fae7ee 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -15,6 +15,7 @@ import "package:photos/db/ml/db.dart"; import 'package:photos/events/local_photos_updated_event.dart'; import "package:photos/extensions/user_extension.dart"; import "package:photos/models/api/collection/user.dart"; +import "package:photos/models/base_location.dart"; import 'package:photos/models/collection/collection.dart'; import 'package:photos/models/collection/collection_items.dart'; import "package:photos/models/file/extensions/file_props.dart"; @@ -23,7 +24,6 @@ import 'package:photos/models/file/file_type.dart'; import "package:photos/models/local_entity_data.dart"; import "package:photos/models/location/location.dart"; import "package:photos/models/location_tag/location_tag.dart"; -import "package:photos/models/memories/memories_cache.dart"; import "package:photos/models/memories/memory.dart"; import "package:photos/models/ml/face/person.dart"; import 'package:photos/models/search/album_search_result.dart'; @@ -1049,6 +1049,42 @@ class SearchService { ); } } + // Add the found base locations from the location/memories service + // TODO: lau: Add base location names + if (limit == null || tagSearchResults.length < limit) { + for (final BaseLocation base in locationService.baseLocations) { + final a = (baseRadius * scaleFactor(base.location.latitude!)) / + kilometersPerDegree; + const b = baseRadius / kilometersPerDegree; + tagSearchResults.add( + GenericSearchResult( + ResultType.location, + "Base", + base.files, + onResultTap: (ctx) { + showAddLocationSheet( + ctx, + base.location, + name: "Base", + radius: baseRadius, + ); + }, + hierarchicalSearchFilter: LocationFilter( + locationTag: LocationTag( + name: "Base", + radius: baseRadius, + centerPoint: base.location, + aSquare: a * a, + bSquare: b * b, + ), + occurrence: kMostRelevantFilter, + matchedUploadedIDs: filesToUploadedFileIDs(base.files), + ), + ), + ); + } + } + if (limit == null || tagSearchResults.length < limit) { final results = await locationService.getFilesInCity(filesWithNoLocTag, ''); @@ -1194,15 +1230,12 @@ class SearchService { BuildContext context, int? limit, ) async { - final fakeCache = MemoriesCache( - toShowMemories: [], - peopleShownLogs: [], - tripsShownLogs: [], - ); - final memories = - await smartMemoriesService.calcMemories(DateTime.now(), fakeCache); + final cache = await memoriesCacheService.debugCacheForTesting(); + final memoriesResult = + await smartMemoriesService.calcMemories(DateTime.now(), cache); + locationService.baseLocations = memoriesResult.baseLocations; final searchResults = []; - for (final memory in memories) { + for (final memory in memoriesResult.memories) { final files = Memory.filesFromMemories(memory.memories); searchResults.add( GenericSearchResult( diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index 8a6ce2a7c0..887fc4eedc 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -34,6 +34,13 @@ import "package:photos/services/machine_learning/ml_result.dart"; import "package:photos/services/machine_learning/semantic_search/semantic_search_service.dart"; import "package:photos/services/search_service.dart"; +class MemoriesResult { + final List memories; + final List baseLocations; + + MemoriesResult(this.memories, this.baseLocations); +} + class SmartMemoriesService { final _logger = Logger("SmartMemoriesService"); final _memoriesDB = MemoriesDB.instance; @@ -73,7 +80,7 @@ class SmartMemoriesService { } // One general method to get all memories, which calls on internal methods for each separate memory type - Future> calcMemories( + Future calcMemories( DateTime now, MemoriesCache oldCache, ) async { @@ -94,7 +101,7 @@ class SmartMemoriesService { _logger.finest("All files length: ${allFiles.length}"); // Trip memories - final tripMemories = + final (tripMemories, bases) = await _getTripsResults(allFiles, now, oldCache.tripsShownLogs); _deductUsedMemories(allFiles, tripMemories); memories.addAll(tripMemories); @@ -109,10 +116,10 @@ class SmartMemoriesService { // Filler memories final fillerMemories = await _getFillerResults(allFiles, now); memories.addAll(fillerMemories); - return memories; + return MemoriesResult(memories, bases); } catch (e, s) { _logger.severe("Error calculating smart memories", e, s); - return []; + return MemoriesResult([], []); } } @@ -476,7 +483,7 @@ class SmartMemoriesService { return memoryResults; } - Future> _getTripsResults( + Future<(List, List)> _getTripsResults( Iterable allFiles, DateTime currentTime, List shownTrips, @@ -484,14 +491,13 @@ class SmartMemoriesService { final List memoryResults = []; final Iterable> locationTagEntities = (await locationService.getLocationTags()); - if (allFiles.isEmpty) return []; + if (allFiles.isEmpty) return ([], []); final nowInMicroseconds = currentTime.microsecondsSinceEpoch; final windowEnd = currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch; final currentMonth = currentTime.month; final cutOffTime = currentTime.subtract(const Duration(days: 365)); - const baseRadius = 0.6; const tripRadius = 100.0; const overlapRadius = 10.0; @@ -767,26 +773,27 @@ class SmartMemoriesService { } // For now for testing let's just surface all base locations - for (final baseLocation in baseLocations) { - String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})"; - final String? locationName = _tryFindLocationName( - Memory.fromFiles(baseLocation.files, _seenTimes), - base: true, - ); - if (locationName != null) { - name = - "$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})"; - } - memoryResults.add( - TripMemory( - Memory.fromFiles(baseLocation.files, _seenTimes), - name, - 0, - 0, - baseLocation.location, - ), - ); - } + // For now surface these on the location section TODO: lau: remove internal flag title + // for (final baseLocation in baseLocations) { + // String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})"; + // final String? locationName = _tryFindLocationName( + // Memory.fromFiles(baseLocation.files, _seenTimes), + // base: true, + // ); + // if (locationName != null) { + // name = + // "$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})"; + // } + // memoryResults.add( + // TripMemory( + // Memory.fromFiles(baseLocation.files, _seenTimes), + // name, + // 0, + // 0, + // baseLocation.location, + // ), + // ); + // } // For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip // Group the trips per month and then year @@ -912,7 +919,7 @@ class SmartMemoriesService { } } } - return memoryResults; + return (memoryResults, baseLocations); } Future> _onThisDayOrWeekResults( From 2a453ee3210cd6e6d7f13fd4574ebc5b91a54c04 Mon Sep 17 00:00:00 2001 From: mngshm Date: Thu, 6 Mar 2025 22:04:31 +0530 Subject: [PATCH 08/19] replication diagram by @maazy4ever --- docs/docs/public/replication.png | Bin 0 -> 77922 bytes docs/docs/self-hosting/guides/configuring-s3.md | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 docs/docs/public/replication.png diff --git a/docs/docs/public/replication.png b/docs/docs/public/replication.png new file mode 100644 index 0000000000000000000000000000000000000000..19fbc1c6a25293ce679c1432d90f5743ada37bf7 GIT binary patch literal 77922 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%0AXhQ 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 contributed diagram of Ente's Replication Process

+ > [!IMPORTANT] > As of now, Replication works only if all the 3 storage type > needs are fulfilled (1 Hot, 1 Cold and 1 Glacier Storage). From 6685c68c35f9ad430bd36edc090963ea4667e212 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 7 Mar 2025 13:03:50 +0530 Subject: [PATCH 09/19] [mob][photos] Show ALL memories in moments section [debug] --- mobile/lib/services/search_service.dart | 4 +- .../lib/services/smart_memories_service.dart | 109 ++++++++++++------ 2 files changed, 73 insertions(+), 40 deletions(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 5e47fae7ee..2f6bf8b4a6 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1231,8 +1231,8 @@ class SearchService { int? limit, ) async { final cache = await memoriesCacheService.debugCacheForTesting(); - final memoriesResult = - await smartMemoriesService.calcMemories(DateTime.now(), cache); + final memoriesResult = await smartMemoriesService + .calcMemories(DateTime.now(), cache, debugSurfaceAll: true); locationService.baseLocations = memoriesResult.baseLocations; final searchResults = []; for (final memory in memoriesResult.memories) { diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index 887fc4eedc..d95050d3c3 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -82,8 +82,9 @@ class SmartMemoriesService { // One general method to get all memories, which calls on internal methods for each separate memory type Future calcMemories( DateTime now, - MemoriesCache oldCache, - ) async { + MemoriesCache oldCache, { + bool debugSurfaceAll = false, + }) async { try { _logger.finest('calcMemories called with time: $now'); await init(); @@ -94,15 +95,17 @@ class SmartMemoriesService { _seenTimes = await _memoriesDB.getSeenTimes(); _logger.finest("All files length: ${allFiles.length}"); - final peopleMemories = - await _getPeopleResults(allFiles, now, oldCache.peopleShownLogs); + final peopleMemories = await _getPeopleResults( + allFiles, now, oldCache.peopleShownLogs, + surfaceAll: debugSurfaceAll,); _deductUsedMemories(allFiles, peopleMemories); memories.addAll(peopleMemories); _logger.finest("All files length: ${allFiles.length}"); // Trip memories - final (tripMemories, bases) = - await _getTripsResults(allFiles, now, oldCache.tripsShownLogs); + final (tripMemories, bases) = await _getTripsResults( + allFiles, now, oldCache.tripsShownLogs, + surfaceAll: debugSurfaceAll,); _deductUsedMemories(allFiles, tripMemories); memories.addAll(tripMemories); _logger.finest("All files length: ${allFiles.length}"); @@ -137,8 +140,9 @@ class SmartMemoriesService { Future> _getPeopleResults( Iterable allFiles, DateTime currentTime, - List shownPeople, - ) async { + List shownPeople, { + bool surfaceAll = false, + }) async { final List memoryResults = []; if (allFiles.isEmpty) return []; final allFileIdsToFile = {}; @@ -365,14 +369,17 @@ class SmartMemoriesService { } } - // // Surface everything just for debug checking - // for (final personID in personToMemories.keys) { - // for (final memoryType in PeopleMemoryType.values) { - // if (personToMemories[personID]!.containsKey(memoryType)) { - // memoryResults.add(personToMemories[personID]![memoryType]!); - // } - // } - // } + // Surface everything just for debug checking + if (surfaceAll) { + for (final personID in personToMemories.keys) { + for (final memoryType in PeopleMemoryType.values) { + if (personToMemories[personID]!.containsKey(memoryType)) { + memoryResults.add(personToMemories[personID]![memoryType]!); + } + } + } + return memoryResults; + } // Loop through the people and check if we should surface anything based on relevancy (bday, last met) personRelevancyLoop: @@ -486,8 +493,9 @@ class SmartMemoriesService { Future<(List, List)> _getTripsResults( Iterable allFiles, DateTime currentTime, - List shownTrips, - ) async { + List shownTrips, { + bool surfaceAll = false, + }) async { final List memoryResults = []; final Iterable> locationTagEntities = (await locationService.getLocationTags()); @@ -774,26 +782,51 @@ class SmartMemoriesService { // For now for testing let's just surface all base locations // For now surface these on the location section TODO: lau: remove internal flag title - // for (final baseLocation in baseLocations) { - // String name = "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})"; - // final String? locationName = _tryFindLocationName( - // Memory.fromFiles(baseLocation.files, _seenTimes), - // base: true, - // ); - // if (locationName != null) { - // name = - // "$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})"; - // } - // memoryResults.add( - // TripMemory( - // Memory.fromFiles(baseLocation.files, _seenTimes), - // name, - // 0, - // 0, - // baseLocation.location, - // ), - // ); - // } + if (surfaceAll) { + for (final baseLocation in baseLocations) { + String name = + "Base (${baseLocation.isCurrentBase ? 'current' : 'old'})"; + final String? locationName = _tryFindLocationName( + Memory.fromFiles(baseLocation.files, _seenTimes), + base: true, + ); + if (locationName != null) { + name = + "$locationName (Base, ${baseLocation.isCurrentBase ? 'current' : 'old'})"; + } + memoryResults.add( + TripMemory( + Memory.fromFiles(baseLocation.files, _seenTimes), + name, + nowInMicroseconds, + windowEnd, + baseLocation.location, + ), + ); + } + for (final trip in validTrips) { + final year = DateTime.fromMicrosecondsSinceEpoch( + trip.averageCreationTime(), + ).year; + final String? locationName = _tryFindLocationName(trip.memories); + String name = "Trip in $year"; + if (locationName != null) { + name = "Trip to $locationName"; + } else if (year == currentTime.year - 1) { + name = "Last year's trip"; + } + final photoSelection = await _bestSelection(trip.memories); + memoryResults.add( + trip.copyWith( + memories: photoSelection, + title: name, + firstDateToShow: nowInMicroseconds, + lastDateToShow: windowEnd, + ), + ); + } + return (memoryResults, baseLocations); + } // For now we surface the two most recent trips of current month, and if none, the earliest upcoming redundant trip // Group the trips per month and then year From de2b399941172e6b28d4c4ce83081c0fe2e69113 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 7 Mar 2025 14:06:58 +0530 Subject: [PATCH 10/19] [mob][photos] datepicker for debugging memories --- mobile/lib/services/search_service.dart | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/mobile/lib/services/search_service.dart b/mobile/lib/services/search_service.dart index 2f6bf8b4a6..f45e2735ed 100644 --- a/mobile/lib/services/search_service.dart +++ b/mobile/lib/services/search_service.dart @@ -1230,9 +1230,21 @@ class SearchService { BuildContext context, int? limit, ) async { + DateTime calcTime = DateTime.now(); + // await two seconds to let new page load first + await Future.delayed(const Duration(seconds: 1)); + if (limit == null) { + final DateTime? pickedTime = await showDatePicker( + context: context, + initialDate: DateTime.now(), + firstDate: DateTime(1900), + lastDate: DateTime(2100), + ); + if (pickedTime != null) calcTime = pickedTime; + } final cache = await memoriesCacheService.debugCacheForTesting(); final memoriesResult = await smartMemoriesService - .calcMemories(DateTime.now(), cache, debugSurfaceAll: true); + .calcMemories(calcTime, cache, debugSurfaceAll: true); locationService.baseLocations = memoriesResult.baseLocations; final searchResults = []; for (final memory in memoriesResult.memories) { From 949909631ad574ee10ade00dfa97ebaff3e3978d Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 7 Mar 2025 14:09:01 +0530 Subject: [PATCH 11/19] [mob][photos] don't sort debug memories --- mobile/lib/models/search/search_types.dart | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/mobile/lib/models/search/search_types.dart b/mobile/lib/models/search/search_types.dart index e3e5e19136..25ee531ad9 100644 --- a/mobile/lib/models/search/search_types.dart +++ b/mobile/lib/models/search/search_types.dart @@ -112,7 +112,11 @@ extension SectionTypeExtensions on SectionType { } } - bool get sortByName => this != SectionType.face && this != SectionType.magic; + // TODO: lau: check if we should sort moment again + bool get sortByName => + this != SectionType.face && + this != SectionType.magic && + this != SectionType.moment; bool get isEmptyCTAVisible { switch (this) { From 920e26255c9e40017e22b3600aaa593488d9586f Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 7 Mar 2025 16:49:47 +0530 Subject: [PATCH 12/19] [mob][photos] Surface calculated persons --- mobile/lib/services/smart_memories_service.dart | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index d95050d3c3..8d98c85daf 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -372,10 +372,9 @@ class SmartMemoriesService { // Surface everything just for debug checking if (surfaceAll) { for (final personID in personToMemories.keys) { - for (final memoryType in PeopleMemoryType.values) { - if (personToMemories[personID]!.containsKey(memoryType)) { - memoryResults.add(personToMemories[personID]![memoryType]!); - } + final personMemories = personToMemories[personID]!; + for (final memoryType in personMemories.keys) { + memoryResults.add(personMemories[memoryType]!); } } return memoryResults; From a68f1e91c5152cfb58ccd6e338da3c66ffc2ee5b Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Fri, 7 Mar 2025 18:18:58 +0530 Subject: [PATCH 13/19] [mob][photos] Performance logging --- .../lib/services/smart_memories_service.dart | 48 +++++++++++++++---- 1 file changed, 39 insertions(+), 9 deletions(-) diff --git a/mobile/lib/services/smart_memories_service.dart b/mobile/lib/services/smart_memories_service.dart index 8d98c85daf..92e2a41383 100644 --- a/mobile/lib/services/smart_memories_service.dart +++ b/mobile/lib/services/smart_memories_service.dart @@ -1,6 +1,7 @@ import "dart:async"; import "dart:math" show min, max; +import "package:flutter/foundation.dart" show kDebugMode; import "package:flutter/material.dart"; import "package:intl/intl.dart"; import "package:logging/logging.dart"; @@ -9,6 +10,7 @@ import "package:photos/core/configuration.dart"; import "package:photos/core/constants.dart"; import "package:photos/db/memories_db.dart"; import "package:photos/db/ml/db.dart"; +import "package:photos/extensions/stop_watch.dart"; import "package:photos/l10n/l10n.dart"; import "package:photos/models/base_location.dart"; import "package:photos/models/file/extensions/file_props.dart"; @@ -86,39 +88,48 @@ class SmartMemoriesService { bool debugSurfaceAll = false, }) async { try { - _logger.finest('calcMemories called with time: $now'); + final TimeLogger t = TimeLogger(context: "calcMemories"); + _logger.finest('calcMemories called with time: $now $t'); await init(); final List memories = []; final allFiles = Set.from( await SearchService.instance.getAllFilesForSearch(), ); _seenTimes = await _memoriesDB.getSeenTimes(); - _logger.finest("All files length: ${allFiles.length}"); + _logger.finest("All files length: ${allFiles.length} $t"); final peopleMemories = await _getPeopleResults( - allFiles, now, oldCache.peopleShownLogs, - surfaceAll: debugSurfaceAll,); + allFiles, + now, + oldCache.peopleShownLogs, + surfaceAll: debugSurfaceAll, + ); _deductUsedMemories(allFiles, peopleMemories); memories.addAll(peopleMemories); - _logger.finest("All files length: ${allFiles.length}"); + _logger.finest("All files length after people: ${allFiles.length} $t"); // Trip memories final (tripMemories, bases) = await _getTripsResults( - allFiles, now, oldCache.tripsShownLogs, - surfaceAll: debugSurfaceAll,); + allFiles, + now, + oldCache.tripsShownLogs, + surfaceAll: debugSurfaceAll, + ); _deductUsedMemories(allFiles, tripMemories); memories.addAll(tripMemories); - _logger.finest("All files length: ${allFiles.length}"); + _logger.finest("All files length after trips: ${allFiles.length} $t"); // Time memories final timeMemories = await _onThisDayOrWeekResults(allFiles, now); _deductUsedMemories(allFiles, timeMemories); memories.addAll(timeMemories); - _logger.finest("All files length: ${allFiles.length}"); + _logger.finest("All files length after time: ${allFiles.length} $t"); // Filler memories final fillerMemories = await _getFillerResults(allFiles, now); + _deductUsedMemories(allFiles, fillerMemories); memories.addAll(fillerMemories); + _logger.finest("All files length after filler: ${allFiles.length} $t"); return MemoriesResult(memories, bases); } catch (e, s) { _logger.severe("Error calculating smart memories", e, s); @@ -143,6 +154,7 @@ class SmartMemoriesService { List shownPeople, { bool surfaceAll = false, }) async { + final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start(); final List memoryResults = []; if (allFiles.isEmpty) return []; final allFileIdsToFile = {}; @@ -154,6 +166,7 @@ class SmartMemoriesService { final nowInMicroseconds = currentTime.microsecondsSinceEpoch; final windowEnd = currentTime.add(kMemoriesUpdateFrequency).microsecondsSinceEpoch; + w?.log('allFiles setup'); // Get ordered list of important people (all named, from most to least files) final persons = await PersonService.instance.getPersons(); @@ -181,6 +194,7 @@ class SmartMemoriesService { final bFaces = personIdToFaceIDs[b]!.length; return bFaces.compareTo(aFaces); }); + w?.log('orderedImportantPersonsID setup'); // Check if the user has assignmed "me" String? meID; @@ -191,6 +205,7 @@ class SmartMemoriesService { break; } } + w?.log('meID setup part 1'); final bool isMeAssigned = meID != null; Map>? meFilesToFaces; if (isMeAssigned) { @@ -199,6 +214,7 @@ class SmartMemoriesService { meFileIDs, ); } + w?.log('meID setup part 2'); // Loop through the people and find all memories final Map> personToMemories = @@ -206,10 +222,12 @@ class SmartMemoriesService { for (final personID in orderedImportantPersonsID) { final personFileIDs = personIdToFileIDs[personID]!; final personName = personIdToPerson[personID]!.data.name; + w?.log('start with new person $personName'); final Map> personFilesToFaces = await MLDataDB.instance.getFacesForFileIDs( personFileIDs, ); + w?.log('personFilesToFaces setup'); // Inside people loop, check for spotlight (Most likely every person will have a spotlight) final spotlightFiles = []; for (final fileID in personFileIDs) { @@ -240,6 +258,7 @@ class SmartMemoriesService { .putIfAbsent(personID, () => {}) .putIfAbsent(PeopleMemoryType.spotlight, () => spotlightMemory); } + w?.log('spotlight setup'); // Inside people loop, check for youAndThem if (isMeAssigned && meID != personID) { @@ -270,12 +289,14 @@ class SmartMemoriesService { .putIfAbsent(personID, () => {}) .putIfAbsent(PeopleMemoryType.youAndThem, () => youAndThemMemory); } + w?.log('youAndThem setup'); } // Inside people loop, check for doingSomethingTogether if (isMeAssigned && meID != personID) { final vectors = await SemanticSearchService.instance .getClipVectorsForFileIDs(personFileIDs); + w?.log('getting clip vectors for doingSomethingTogether'); final activityFiles = []; PeopleActivity lastActivity = PeopleActivity.values.first; activityLoop: @@ -289,6 +310,9 @@ class SmartMemoriesService { } final similarities = await MLComputer.instance .compareEmbeddings(vectors, activityVector); + w?.log( + 'comparing embeddings for doingSomethingTogether and $activity', + ); for (final fileID in personFileIDs) { final similarity = similarities[fileID]; if (similarity == null) continue; @@ -319,6 +343,7 @@ class SmartMemoriesService { () => activityMemory, ); } + w?.log('doingSomethingTogether setup'); } // Inside people loop, check for lastTimeYouSawThem @@ -367,6 +392,7 @@ class SmartMemoriesService { () => lastTimeMemory, ); } + w?.log('lastTimeYouSawThem setup'); } // Surface everything just for debug checking @@ -446,6 +472,7 @@ class SmartMemoriesService { } } } + w?.log('relevancy setup'); // Loop through the people (and memory types) and add based on rotation if (memoryResults.length >= 3) return memoryResults; @@ -485,6 +512,7 @@ class SmartMemoriesService { } if (added > 0) break peopleRotationLoop; } + w?.log('rotation setup'); return memoryResults; } @@ -1285,6 +1313,7 @@ class SmartMemoriesService { int? prefferedSize, }) async { try { + final w = (kDebugMode ? EnteWatch('getPeopleResults') : null)?..start(); final fileCount = memories.length; final int targetSize = prefferedSize ?? 10; if (fileCount <= targetSize) return memories; @@ -1379,6 +1408,7 @@ class SmartMemoriesService { _logger.finest( 'People memories selection done, returning ${finalSelection.length} memories', ); + w?.log('People memories selection done'); return finalSelection; } catch (e, s) { _logger.severe('Error in _bestSelectionPeople', e, s); From 26cb6ad7223834e9140ef4e899cdaf4817b2dfba Mon Sep 17 00:00:00 2001 From: mngshm Date: Mon, 10 Mar 2025 11:58:59 +0530 Subject: [PATCH 14/19] [server] avoid scary error msgs if payment/billing configuration is not found --- server/pkg/controller/offer/offer.go | 2 +- server/pkg/utils/billing/billing.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/server/pkg/controller/offer/offer.go b/server/pkg/controller/offer/offer.go index 94235d74b3..0a313ecd11 100644 --- a/server/pkg/controller/offer/offer.go +++ b/server/pkg/controller/offer/offer.go @@ -42,7 +42,7 @@ func NewOfferController( blackFridayOffers := make(ente.BlackFridayOfferPerCountry) path, err := config.BillingConfigFilePath("black-friday.json") if err != nil { - log.Fatalf("Error getting offer config file: %v", err) + log.Fatalf("Skipping BF configuration, config file not found: %v", err) } data, err := os.ReadFile(path) if err != nil { diff --git a/server/pkg/utils/billing/billing.go b/server/pkg/utils/billing/billing.go index e6e0e6a718..b2d012012d 100644 --- a/server/pkg/utils/billing/billing.go +++ b/server/pkg/utils/billing/billing.go @@ -88,7 +88,7 @@ func parsePricingFile(fileName string) ente.BillingPlansPerCountry { } data, err := os.ReadFile(filePath) if err != nil { - logrus.Errorf("Error reading file %s: %v\n", filePath, err) + logrus.Errorf("Skipping payment configuration, (config file not found): %v\n", err) return nil } From a640a430bfaf9d7299225f22eae50969f3ab0b2e Mon Sep 17 00:00:00 2001 From: mngshm Date: Mon, 10 Mar 2025 12:08:08 +0530 Subject: [PATCH 15/19] tweak --- server/pkg/utils/billing/billing.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pkg/utils/billing/billing.go b/server/pkg/utils/billing/billing.go index b2d012012d..6c14c8450d 100644 --- a/server/pkg/utils/billing/billing.go +++ b/server/pkg/utils/billing/billing.go @@ -88,7 +88,7 @@ func parsePricingFile(fileName string) ente.BillingPlansPerCountry { } data, err := os.ReadFile(filePath) if err != nil { - logrus.Errorf("Skipping payment configuration, (config file not found): %v\n", err) + logrus.Errorf("Skipping payment configuration, pricing data unavailable in config: %v\n", err) return nil } From 0f0790df5f5e403e74d10af7d0e65fe69a6e0845 Mon Sep 17 00:00:00 2001 From: Iiii-I-I-I Date: Mon, 10 Mar 2025 02:54:38 -0400 Subject: [PATCH 16/19] [Auth] Add two custom icons (#5267) ## Description Add icons for: * Federal Student Aid ([studentaid.gov](https://studentaid.gov)), rank 557 on [Similarweb](https://www.similarweb.com/website/studentaid.gov/) * RuneScape Wiki ([runescape.wiki](https://runescape.wiki/)), rank 2,652 on [Similarweb](https://www.similarweb.com/website/runescape.wiki/) --- auth/assets/custom-icons/_data/custom-icons.json | 12 ++++++++++++ .../custom-icons/icons/federal_student_aid.svg | 6 ++++++ auth/assets/custom-icons/icons/runescape_wiki.svg | 7 +++++++ 3 files changed, 25 insertions(+) create mode 100644 auth/assets/custom-icons/icons/federal_student_aid.svg create mode 100644 auth/assets/custom-icons/icons/runescape_wiki.svg diff --git a/auth/assets/custom-icons/_data/custom-icons.json b/auth/assets/custom-icons/_data/custom-icons.json index 73a0ac3142..fe87ab1e58 100644 --- a/auth/assets/custom-icons/_data/custom-icons.json +++ b/auth/assets/custom-icons/_data/custom-icons.json @@ -379,6 +379,14 @@ { "title": "Fastmail" }, + { + "title": "Federal Student Aid", + "slug": "federal_student_aid", + "altNames": [ + "FSA", + "FAFSA" + ] + }, { "title": "Fidelity", "slug": "fidelity", @@ -952,6 +960,10 @@ { "title": "RuneMate" }, + { + "title": "RuneScape Wiki", + "slug": "runescape_wiki" + }, { "title": "Rust Language Forum", "slug": "rust_language_forum", diff --git a/auth/assets/custom-icons/icons/federal_student_aid.svg b/auth/assets/custom-icons/icons/federal_student_aid.svg new file mode 100644 index 0000000000..945ade25cc --- /dev/null +++ b/auth/assets/custom-icons/icons/federal_student_aid.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/auth/assets/custom-icons/icons/runescape_wiki.svg b/auth/assets/custom-icons/icons/runescape_wiki.svg new file mode 100644 index 0000000000..59d2addb3a --- /dev/null +++ b/auth/assets/custom-icons/icons/runescape_wiki.svg @@ -0,0 +1,7 @@ + + + + + + + From dd807368b2893fb4b77f797d9699ede0f3823cf0 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Mon, 10 Mar 2025 12:41:36 +0530 Subject: [PATCH 17/19] [auth] New translations (#5266) New translations from [Crowdin](https://crowdin.com/project/ente-authenticator-app) Co-authored-by: Crowdin Bot --- auth/lib/l10n/arb/app_es.arb | 2 ++ auth/lib/l10n/arb/app_it.arb | 1 + auth/lib/l10n/arb/app_ja.arb | 1 + auth/lib/l10n/arb/app_lt.arb | 1 + auth/lib/l10n/arb/app_ml.arb | 3 +++ auth/lib/l10n/arb/app_sl.arb | 29 ++++++++++++++++++++++++++++- auth/lib/l10n/arb/app_sv.arb | 26 ++++++++++++++++++++++++++ 7 files changed, 62 insertions(+), 1 deletion(-) diff --git a/auth/lib/l10n/arb/app_es.arb b/auth/lib/l10n/arb/app_es.arb index c224d6677c..cd9506b943 100644 --- a/auth/lib/l10n/arb/app_es.arb +++ b/auth/lib/l10n/arb/app_es.arb @@ -499,6 +499,7 @@ "appLockOfflineModeWarning": "Has elegido proceder sin copia de seguridad. Si olvidas el código de desbloqueo de la aplicación, se bloqueará el acceso a sus datos.", "duplicateCodes": "Duplicar códigos", "noDuplicates": "✨ No hay duplicados", + "youveNoDuplicateCodesThatCanBeCleared": "No tienes códigos duplicados que se puedan borrar", "deduplicateCodes": "Desduplicar códigos", "deselectAll": "Deseleccionar todo", "selectAll": "Seleccionar todo", @@ -509,6 +510,7 @@ "supportEnte": "Apoya a ente", "giveUsAStarOnGithub": "Danos una estrella en GitHub", "free5GB": "5 GB gratis en ente Fotos", + "loginWithAuthAccount": "Inicia sesión con tu cuenta de Auth", "freeStorageOffer": "10% de descuento en ente fotos", "freeStorageOfferDescription": "Usa el cupón \"AUTH\" para obtener un 10% de descuento en el primer año" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_it.arb b/auth/lib/l10n/arb/app_it.arb index c68a87301b..17281db08a 100644 --- a/auth/lib/l10n/arb/app_it.arb +++ b/auth/lib/l10n/arb/app_it.arb @@ -499,6 +499,7 @@ "appLockOfflineModeWarning": "Hai scelto di procedere senza backup. Se dimentichi il tuo codice di blocco dell'app, non potrai più accedere ai tuoi dati.", "duplicateCodes": "Codici duplicati", "noDuplicates": "✨ Nessun doppione", + "youveNoDuplicateCodesThatCanBeCleared": "Non ci sono codici duplicati che possono essere cancellati", "deduplicateCodes": "Codici deduplicati", "deselectAll": "Deselezionare tutti", "selectAll": "Seleziona tutti", diff --git a/auth/lib/l10n/arb/app_ja.arb b/auth/lib/l10n/arb/app_ja.arb index 49cd9a1e84..086d8afe32 100644 --- a/auth/lib/l10n/arb/app_ja.arb +++ b/auth/lib/l10n/arb/app_ja.arb @@ -499,6 +499,7 @@ "appLockOfflineModeWarning": "バックアップなしで進むことを選択しました。アプリロックを忘れると、データにアクセスできなくなります。", "duplicateCodes": "重複コード", "noDuplicates": "✨ 重複なし", + "youveNoDuplicateCodesThatCanBeCleared": "削除できる重複コードはありません", "deduplicateCodes": "重複コード", "deselectAll": "すべての選択を解除", "selectAll": "すべて選択", diff --git a/auth/lib/l10n/arb/app_lt.arb b/auth/lib/l10n/arb/app_lt.arb index 71b96d3027..f1903dd9b0 100644 --- a/auth/lib/l10n/arb/app_lt.arb +++ b/auth/lib/l10n/arb/app_lt.arb @@ -499,6 +499,7 @@ "appLockOfflineModeWarning": "Pasirinkote tęsti be atsarginių kopijų. Jei pamiršite programos užraktą, jums bus užrakinta prieiga prie duomenų.", "duplicateCodes": "Dubliuoti kodus", "noDuplicates": "✨ Dublikatų nėra", + "youveNoDuplicateCodesThatCanBeCleared": "Neturite dubliuotų kodų, kuriuos būtų galima išvalyti.", "deduplicateCodes": "Atdubliuoti kodus", "deselectAll": "Naikinti visų pasirinkimą", "selectAll": "Pasirinkti viską", diff --git a/auth/lib/l10n/arb/app_ml.arb b/auth/lib/l10n/arb/app_ml.arb index ed441ead94..67f7dba5ce 100644 --- a/auth/lib/l10n/arb/app_ml.arb +++ b/auth/lib/l10n/arb/app_ml.arb @@ -1,4 +1,7 @@ { + "account": "അക്കൗണ്ട്", + "unlock": "അൺലോക്ക്", + "qrCode": "QR കോഡ്", "blog": "ബ്ലോഗ്", "verifyPassword": "പാസ്‌വേഡ് സ്ഥിരീകരിക്കുക", "recreatePassword": "പാസ്‌വേഡ് പുനഃസൃഷ്ടിക്കുക", diff --git a/auth/lib/l10n/arb/app_sl.arb b/auth/lib/l10n/arb/app_sl.arb index 0be8735800..2386ed546c 100644 --- a/auth/lib/l10n/arb/app_sl.arb +++ b/auth/lib/l10n/arb/app_sl.arb @@ -88,6 +88,8 @@ "useRecoveryKey": "Uporabi ključ za obnovo", "incorrectPasswordTitle": "Nepravilno geslo", "welcomeBack": "Dobrodošli nazaj!", + "emailAlreadyRegistered": "E-poštni naslov je že registriran.", + "emailNotRegistered": "E-poštni naslov ni registriran.", "madeWithLoveAtPrefix": "ustvarjeno s ❤️pri ", "supportDevs": "Naročite se na ente, da nas podprete", "supportDiscount": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto", @@ -156,6 +158,7 @@ "twoFactorAuthTitle": "Dvojno preverjanja pristnosti", "passkeyAuthTitle": "Potrditev ključa za dostop (passkey)", "verifyPasskey": "Potrdite ključ za dostop (passkey)", + "loginWithTOTP": "Prijava z TOTP", "recoverAccount": "Obnovi račun", "enterRecoveryKeyHint": "Vnesite vaš ključ za obnovitev", "recover": "Obnovi", @@ -257,6 +260,10 @@ "areYouSureYouWantToLogout": "Ali ste prepričani, da se želite odjaviti?", "yesLogout": "Ja, odjavi se", "exit": "Izhod", + "theme": "Tema", + "lightTheme": "Svetla", + "darkTheme": "Temna", + "systemTheme": "Sistemska", "verifyingRecoveryKey": "Preverjanje ključa za obnovitev", "recoveryKeyVerified": "Ključ za obnovitev preverjen", "recoveryKeySuccessBody": "Odlično! Vaš ključ za obnovitev je veljaven. Hvala za preverjanje.\n\nNe pozabite shraniti varnostno kopijo obnovitvenega ključa.", @@ -327,6 +334,8 @@ } } }, + "manualSort": "Po meri", + "editOrder": "Uredi vrstni red", "mostFrequentlyUsed": "Pogosto uporabljeni", "mostRecentlyUsed": "Nedavno uporabljeno", "activeSessions": "Aktivne seje", @@ -448,6 +457,8 @@ "customEndpoint": "Povezano na {endpoint}", "pinText": "Pripni", "unpinText": "Odpni", + "pinnedCodeMessage": "{code} je bila pripeta", + "unpinnedCodeMessage": "{code} je bila odpeta", "pinned": "Pripeto", "tags": "Oznake", "createNewTag": "Ustvari novo oznako", @@ -485,5 +496,21 @@ "appLockNotEnabled": "Zaklepanje aplikacije ni omogočeno", "appLockNotEnabledDescription": "Prosimo, omogočite zaklepanje aplikacije v Nastavitve > Zaklepanje Aplikacije (Security > App Lock)", "authToViewPasskey": "Da vidite passkey, se overite", - "appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen." + "appLockOfflineModeWarning": "Odločili ste se, da boste nadaljevali brez varnostnih kopij. Če boste pozabili geslo za odklepanje aplikacije, bo dostop do vaših podatkov onemogočen.", + "duplicateCodes": "Podvojene kode", + "noDuplicates": "✨ Ni duplikatov", + "youveNoDuplicateCodesThatCanBeCleared": "Nimate nobenih podvojenih kod, ki bi jih bilo mogoče izbrisati", + "deduplicateCodes": "Dedupliciraj kode", + "deselectAll": "Prekliči celoten izbor", + "selectAll": "Izberi vse", + "deleteDuplicates": "Izbriši dvojnike", + "plainHTML": "Navadni HTML", + "tellUsWhatYouThink": "Povejte nam kaj mislite", + "dropReview": "Napišite oceno v trgovini App/Play Store", + "supportEnte": "Podpiraj ente", + "giveUsAStarOnGithub": "Dajte nam zvezdico na Githubu", + "free5GB": "5 GB zastonj na ente fotografije", + "loginWithAuthAccount": "Prijavite se s svojim Auth računom", + "freeStorageOffer": "10 % popust na ente fotografije", + "freeStorageOfferDescription": "Uporabite kupon \"AUTH\" za 10% popusta za prvo leto" } \ No newline at end of file diff --git a/auth/lib/l10n/arb/app_sv.arb b/auth/lib/l10n/arb/app_sv.arb index facf5c1c1e..a3fc9fa795 100644 --- a/auth/lib/l10n/arb/app_sv.arb +++ b/auth/lib/l10n/arb/app_sv.arb @@ -267,7 +267,9 @@ "verifyingRecoveryKey": "Verifierar återställningsnyckel...", "recoveryKeyVerified": "Återställningsnyckel verifierad", "recoveryKeySuccessBody": "Grymt! Din återställningsnyckel är giltig. Tack för att du verifierade.\n\nKom ihåg att hålla din återställningsnyckel säker med backups.", + "invalidRecoveryKey": "Återställningsnyckeln du angav är inte giltig. Kontrollera att den innehåller 24 ord och kontrollera stavningen av varje ord.\n\nOm du har angett en äldre återställningskod, se till att den är 64 tecken lång, och kontrollera var och en av bokstäverna.", "recreatePasswordTitle": "Återskapa lösenord", + "recreatePasswordBody": "Denna enhet är inte tillräckligt kraftfull för att verifiera ditt lösenord, men vi kan återskapa det på ett sätt som fungerar med alla enheter.\n\nLogga in med din återställningsnyckel och återskapa ditt lösenord (du kan använda samma igen om du vill).", "invalidKey": "Ogiltig nyckel", "tryAgain": "Försök igen", "viewRecoveryKey": "Visa återställningsnyckel", @@ -279,6 +281,10 @@ "copyEmailAddress": "Kopiera e-postadress", "exportLogs": "Exportera loggar", "enterYourRecoveryKey": "Ange din återställningsnyckel", + "tempErrorContactSupportIfPersists": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.", + "networkHostLookUpErr": "Det gick inte att ansluta till Ente, kontrollera dina nätverksinställningar och kontakta supporten om felet kvarstår.", + "networkConnectionRefusedErr": "Det gick inte att ansluta till Ente, försök igen om en stund. Om felet kvarstår, vänligen kontakta support.", + "itLooksLikeSomethingWentWrongPleaseRetryAfterSome": "Det ser ut som om något gick fel. Försök igen efter en stund. Om felet kvarstår, vänligen kontakta vår support.", "about": "Om", "weAreOpenSource": "Vi är öppen källkod!", "privacy": "Sekretess", @@ -292,6 +298,7 @@ "checking": "Kontrollerar ...", "youAreOnTheLatestVersion": "Du är på den senaste versionen", "warning": "Varning", + "exportWarningDesc": "Den exporterade filen innehåller känslig information. Förvara den på ett säkert sätt.", "iUnderStand": "Jag förstår", "@iUnderStand": { "description": "Text for the button to confirm the user understands the warning" @@ -309,28 +316,46 @@ } }, "sorry": "Tyvärr", + "importFailureDesc": "Det gick inte att tolka den valda filen.\nSkriv till support@ente.io om du behöver hjälp!", "pendingSyncs": "Varning", + "pendingSyncsWarningBody": "En del av dina koder har inte säkerhetskopierats.\n\nSe till att du har en säkerhetskopia för dessa koder innan du loggar ut.", "checkInboxAndSpamFolder": "Vänligen kontrollera din inkorg (och skräppost) för att slutföra verifieringen", "tapToEnterCode": "Tryck för att ange kod", "resendEmail": "Skicka e-post igen", + "weHaveSendEmailTo": "Vi har skickat ett mail till {email}", + "@weHaveSendEmailTo": { + "description": "Text to indicate that we have sent a mail to the user", + "placeholders": { + "email": { + "description": "The email address of the user", + "type": "String", + "example": "example@ente.io" + } + } + }, "manualSort": "Anpassad", "editOrder": "Redigera ordning", "mostFrequentlyUsed": "Ofta använd", "mostRecentlyUsed": "Senast använd", "activeSessions": "Aktiva sessioner", + "somethingWentWrongPleaseTryAgain": "Något gick fel, vänligen försök igen", "thisWillLogYouOutOfThisDevice": "Detta kommer att logga ut dig från den här enheten!", + "thisWillLogYouOutOfTheFollowingDevice": "Detta kommer att logga ut dig från följande enhet:", "terminateSession": "Avsluta session?", "terminate": "Avsluta", "thisDevice": "Den här enheten", + "toResetVerifyEmail": "För att återställa ditt lösenord måste du först bekräfta din e-postadress.", "thisEmailIsAlreadyInUse": "Denna e-postadress används redan", "verificationFailedPleaseTryAgain": "Verifiering misslyckades, vänligen försök igen", "yourVerificationCodeHasExpired": "Din verifieringskod har upphört att gälla", "incorrectCode": "Felaktig kod", "sorryTheCodeYouveEnteredIsIncorrect": "Tyvärr, den kod som du har angett är felaktig", + "emailChangedTo": "E-post ändrad till {newEmail}", "authenticationFailedPleaseTryAgain": "Autentisering misslyckades, vänligen försök igen", "authenticationSuccessful": "Autentisering lyckades!", "twofactorAuthenticationSuccessfullyReset": "Tvåfaktorsautentisering återställd", "incorrectRecoveryKey": "Felaktig återställningsnyckel", + "theRecoveryKeyYouEnteredIsIncorrect": "Återställningsnyckeln du angav är felaktig", "enterPassword": "Ange lösenord", "selectExportFormat": "Välj exportformat", "encrypted": "Krypterad", @@ -343,6 +368,7 @@ "showLargeIcons": "Visa stora ikoner", "compactMode": "Kompakt läge", "shouldHideCode": "Dölj koder", + "doubleTapToViewHiddenCode": "Du kan dubbeltrycka på en post för att visa koden", "focusOnSearchBar": "Fokusera på sök vid appstart", "minimizeAppOnCopy": "Minimera appen vid kopiering", "editCodeAuthMessage": "Autentisera för att redigera kod", From d88b39ec463356e8614c2d572b8d8c1a2586632e Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Mon, 10 Mar 2025 13:26:11 +0530 Subject: [PATCH 18/19] [mob][photos] bump for internal release --- mobile/pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/pubspec.yaml b/mobile/pubspec.yaml index 9956bb03d2..25616d1458 100644 --- a/mobile/pubspec.yaml +++ b/mobile/pubspec.yaml @@ -12,7 +12,7 @@ description: ente photos application # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 0.9.99+1010 +version: 0.9.100+1012 publish_to: none environment: From 300b3c89a39d1c7d71fdd9150cfdd0266de6eeb4 Mon Sep 17 00:00:00 2001 From: Vishnu Mohandas Date: Mon, 10 Mar 2025 13:33:52 +0530 Subject: [PATCH 19/19] Update README.md --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 9fd8fe8daa..c59ee59f19 100644 --- a/README.md +++ b/README.md @@ -95,8 +95,8 @@ please see our [support guide](SUPPORT.md). Ente's Mascot, Ducky,
     inviting people to Ente's source code repository -Please visit our [community page](https://ente.io/community) for all the ways to -connect with the community. +Please visit the [community section](https://ente.io/about#community) for all the ways to +connect with our community. [![Discord](https://img.shields.io/discord/948937918347608085?style=for-the-badge&logo=Discord&logoColor=white&label=Discord)](https://discord.gg/z2YVKkycX3) [![Ente's Blog RSS](https://img.shields.io/badge/blog-rss-F88900?style=for-the-badge&logo=rss&logoColor=white)](https://ente.io/blog/rss.xml)