From 57ec62e45eb60f7ed9ff889c397093565fa1be05 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Fri, 11 Jul 2025 17:00:23 +0530 Subject: [PATCH 01/29] Implement advance date search service --- .../lib/services/date_parse_service.dart | 372 ++++++++++++++++++ .../photos/lib/services/search_service.dart | 121 +++--- 2 files changed, 429 insertions(+), 64 deletions(-) create mode 100644 mobile/apps/photos/lib/services/date_parse_service.dart diff --git a/mobile/apps/photos/lib/services/date_parse_service.dart b/mobile/apps/photos/lib/services/date_parse_service.dart new file mode 100644 index 0000000000..01cd898f19 --- /dev/null +++ b/mobile/apps/photos/lib/services/date_parse_service.dart @@ -0,0 +1,372 @@ +import "package:photos/data/months.dart"; +import 'package:tuple/tuple.dart'; + +class DateParseService { + static final DateParseService instance = + DateParseService._privateConstructor(); + DateParseService._privateConstructor(); + + static const Map _monthMap = { + "january": 1, "february": 2, "march": 3, "april": 4, + "may": 5, "june": 6, "july": 7, "august": 8, + "september": 9, "october": 10, "november": 11, "december": 12, + "jan": 1, "feb": 2, "mar": 3, "apr": 4, "jun": 6, + "jul": 7, "aug": 8, "sep": 9, "sept": 9, "oct": 10, "nov": 11, "dec": 12, + "janu": 1, "febr": 2, "marc": 3, "apri": 4, "juli": 7, + "augu": 8, "sepe": 9, "octo": 10, "nove": 11, "dece": 12, + }; + + static const Map monthNumberToName = { + 1: "January", + 2: "February", + 3: "March", + 4: "April", + 5: "May", + 6: "June", + 7: "July", + 8: "August", + 9: "September", + 10: "October", + 11: "November", + 12: "December", + }; + + String normalizeDateString(String input) { + return input + .toLowerCase() + .replaceAllMapped( + RegExp(r'\b(\d{1,2})(st|nd|rd|th)\b'), + (match) => match.group(1)!, + ) + .replaceAll(RegExp(r'\bof\b'), '') + .replaceAll(RegExp(r'[,\.]+'), '') + .replaceAll(RegExp(r'\s+'), ' ') + .trim(); + } + + Tuple3 parseStructuredFormats(String input) { + final normalized = input.replaceAll(RegExp(r'\s'), ''); + + // ISO format: YYYY-MM-DD or YYYY/MM/DD + final isoMatch = + RegExp(r'^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$').firstMatch(normalized); + if (isoMatch != null) { + return Tuple3( + int.tryParse(isoMatch.group(3)!), // day + int.tryParse(isoMatch.group(2)!), // month + int.tryParse(isoMatch.group(1)!), // year + ); + } + + // Standard formats: MM/DD/YYYY, DD/MM/YYYY, MM-DD-YYYY, DD-MM-YYYY + final standardMatch = + RegExp(r'^(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})$').firstMatch(normalized); + if (standardMatch != null) { + final first = int.tryParse(standardMatch.group(1)!); + final second = int.tryParse(standardMatch.group(2)!); + final year = int.tryParse(standardMatch.group(3)!); + + if (first == null || second == null || year == null) { + return const Tuple3(null, null, null); + } + if (first < 1 || first > 31 || second < 1 || second > 31) { + return const Tuple3(null, null, null); + } + + // Heuristic: if first number > 12, assume DD/MM format + if (first > 12) { + if (second > 12) return const Tuple3(null, null, null); + return Tuple3(first, second, year); + } else { + if (first > 12) return const Tuple3(null, null, null); + return Tuple3(second, first, year); + } + } + + // Standard formats with 2-digit years: MM/DD/YY, DD/MM/YY, MM-DD-YY, DD-MM-YY + final standardMatchTwoDigitYear = + RegExp(r'^(\d{1,2})[\/-](\d{1,2})[\/-](\d{2})$').firstMatch(normalized); + if (standardMatchTwoDigitYear != null) { + final first = int.tryParse(standardMatchTwoDigitYear.group(1)!); + final second = int.tryParse(standardMatchTwoDigitYear.group(2)!); + final yearTwoDigit = int.tryParse(standardMatchTwoDigitYear.group(3)!); + + if (first == null || second == null || yearTwoDigit == null) { + return const Tuple3(null, null, null); + } + if (first < 1 || first > 31 || second < 1 || second > 31) { + return const Tuple3(null, null, null); + } + + final year = + yearTwoDigit < 50 ? 2000 + yearTwoDigit : 1900 + yearTwoDigit; + + if (first > 12) { + if (second > 12) return const Tuple3(null, null, null); + return Tuple3(first, second, year); + } else { + if (first > 12) return const Tuple3(null, null, null); + return Tuple3(second, first, year); + } + } + + // Dot format: DD.MM.YYYY + final dotMatch = + RegExp(r'^(\d{1,2})\.(\d{1,2})\.(\d{4})$').firstMatch(normalized); + if (dotMatch != null) { + final day = int.tryParse(dotMatch.group(1)!); + final month = int.tryParse(dotMatch.group(2)!); + final year = int.tryParse(dotMatch.group(3)!); + + if (day == null || month == null || year == null) { + return const Tuple3(null, null, null); + } + if (day < 1 || day > 31 || month < 1 || month > 12) { + return const Tuple3(null, null, null); + } + + return Tuple3(day, month, year); + } + + // Dot format with 2-digit year: DD.MM.YY + final dotMatchTwoDigitYear = + RegExp(r'^(\d{1,2})\.(\d{1,2})\.(\d{2})$').firstMatch(normalized); + if (dotMatchTwoDigitYear != null) { + final day = int.tryParse(dotMatchTwoDigitYear.group(1)!); + final month = int.tryParse(dotMatchTwoDigitYear.group(2)!); + final yearTwoDigit = int.tryParse(dotMatchTwoDigitYear.group(3)!); + + if (day == null || month == null || yearTwoDigit == null) { + return const Tuple3(null, null, null); + } + if (day < 1 || day > 31 || month < 1 || month > 12) { + return const Tuple3(null, null, null); + } + + final year = + yearTwoDigit < 50 ? 2000 + yearTwoDigit : 1900 + yearTwoDigit; + + return Tuple3(day, month, year); + } + + // Compact format: YYYYMMDD + if (normalized.length == 8 && RegExp(r'^\d{8}$').hasMatch(normalized)) { + final year = int.tryParse(normalized.substring(0, 4)); + final month = int.tryParse(normalized.substring(4, 6)); + final day = int.tryParse(normalized.substring(6, 8)); + if (year != null && + year > 1900 && + month != null && + month <= 12 && + day != null && + day <= 31) { + return Tuple3(day, month, year); + } + } + + // Short format: MM/DD or DD/MM + final shortMatch = + RegExp(r'^(\d{1,2})[\/-](\d{1,2})$').firstMatch(normalized); + if (shortMatch != null) { + final first = int.tryParse(shortMatch.group(1)!); + final second = int.tryParse(shortMatch.group(2)!); + + if (first == null || second == null) { + return const Tuple3(null, null, null); + } + if (first < 1 || first > 31 || second < 1 || second > 31) { + return const Tuple3(null, null, null); + } + + if (first > 12) { + if (second > 12) return const Tuple3(null, null, null); + return Tuple3(first, second, null); + } else { + if (first > 12) return const Tuple3(null, null, null); + return Tuple3(second, first, null); + } + } + + return const Tuple3(null, null, null); + } + + Tuple3 _parseDateParts(String input) { + if (input.trim().isEmpty) return const Tuple3(null, null, null); + + final lowerInput = input.toLowerCase(); + final today = DateTime.now(); + + if (lowerInput.contains('today')) { + return Tuple3( + today.day, + MonthData(monthNumberToName[today.month]!, today.month), + today.year, + ); + } else if (lowerInput.contains('tomorrow')) { + final tomorrow = today.add(const Duration(days: 1)); + return Tuple3( + tomorrow.day, + MonthData(monthNumberToName[tomorrow.month]!, tomorrow.month), + tomorrow.year, + ); + } else if (lowerInput.contains('yesterday')) { + final yesterday = today.subtract(const Duration(days: 1)); + return Tuple3( + yesterday.day, + MonthData(monthNumberToName[yesterday.month]!, yesterday.month), + yesterday.year, + ); + } + + // Check for year-only queries like "2025" + final yearOnlyMatch = RegExp(r'^\s*(\d{4})\s*$').firstMatch(input.trim()); + if (yearOnlyMatch != null) { + final year = int.tryParse(yearOnlyMatch.group(1)!); + if (year != null && year >= 1900 && year <= 2100) { + return Tuple3(null, null, year); + } + } + + // First try structured formats (slash, dash, dot patterns) + final structuredResult = parseStructuredFormats(input); + if (structuredResult.item1 != null || + structuredResult.item2 != null || + structuredResult.item3 != null) { + final int? day = structuredResult.item1; + final int? monthNum = structuredResult.item2; + final int? year = structuredResult.item3; + MonthData? monthData; + if (monthNum != null && monthNumberToName.containsKey(monthNum)) { + monthData = MonthData(monthNumberToName[monthNum]!, monthNum); + } + return Tuple3(day, monthData, year); + } + + final normalized = normalizeDateString(input); + final tokens = normalized.split(RegExp(r'\s+')); + + int? day, monthNum, year; + MonthData? monthData; + + // Handle patterns like "25 02" (day month) or "Feb 2025" (month year) + if (tokens.length == 2) { + final first = tokens[0]; + final second = tokens[1]; + + // Check if first token is a month name and second is a year + if (_monthMap.containsKey(first)) { + final yearValue = int.tryParse(second); + if (yearValue != null && yearValue >= 1900 && yearValue <= 2100) { + monthNum = _monthMap[first]; + year = yearValue; + } + } + // Check if both are numbers - could be day+month or month+day + else { + final firstNum = int.tryParse(first); + final secondNum = int.tryParse(second); + + if (firstNum != null && secondNum != null) { + if (secondNum >= 1900 && secondNum <= 2100) { + if (firstNum >= 1 && firstNum <= 12) { + monthNum = firstNum; + year = secondNum; + } + } else if (firstNum >= 1 && + firstNum <= 31 && + secondNum >= 1 && + secondNum <= 12) { + day = firstNum; + monthNum = secondNum; + } else if (firstNum >= 1 && + firstNum <= 12 && + secondNum >= 1 && + secondNum <= 31) { + monthNum = firstNum; + day = secondNum; + } + } + } + } + + if (day == null && monthNum == null && year == null) { + for (var token in tokens) { + if (_monthMap.containsKey(token)) { + monthNum = _monthMap[token]; + } else if (RegExp(r'^\d+$').hasMatch(token)) { + final value = int.parse(token); + if (value >= 1900 && value <= 2100) { + year = value; + } else if (value >= 1 && value <= 31) { + if (day == null) { + day = value; + } else if (monthNum == null && value <= 12) { + monthNum = value; + } + } else if (value >= 32 && value <= 99) { + year = value < 50 ? 2000 + value : 1900 + value; + } + } + } + } + + if (monthNum != null && monthNumberToName.containsKey(monthNum)) { + monthData = MonthData(monthNumberToName[monthNum]!, monthNum); + } + + if (monthNum != null && (monthNum < 1 || monthNum > 12)) { + monthNum = null; + monthData = null; + } + if (day != null && (day < 1 || day > 31)) { + day = null; + } + + return Tuple3(day, monthData, year); + } + + Tuple3 parseDate(String input) { + return _parseDateParts(input); + } + + List> parseDateVariations(String input) { + final List> variations = []; + final primaryResult = _parseDateParts(input); + + variations.add(primaryResult); + + if (primaryResult.item1 != null && + primaryResult.item2 != null && + primaryResult.item3 != null) { + final genericVersion = Tuple3( + primaryResult.item1, + primaryResult.item2, + null, + ); + if (!variations.contains(genericVersion)) { + variations.add(genericVersion); + } + } + + return variations; + } + + bool isYearQuery(String input) { + final yearOnlyMatch = RegExp(r'^\s*(\d{4})\s*$').firstMatch(input.trim()); + if (yearOnlyMatch != null) { + final year = int.tryParse(yearOnlyMatch.group(1)!); + return year != null && year >= 1900 && year <= 2100; + } + return false; + } + + bool isMonthYearQuery(String input) { + final result = _parseDateParts(input); + return result.item1 == null && result.item2 != null && result.item3 != null; + } + + bool isGenericDateQuery(String input) { + final result = _parseDateParts(input); + return result.item1 != null && result.item2 != null && result.item3 == null; + } +} diff --git a/mobile/apps/photos/lib/services/search_service.dart b/mobile/apps/photos/lib/services/search_service.dart index f0429ed947..8c77d10d8e 100644 --- a/mobile/apps/photos/lib/services/search_service.dart +++ b/mobile/apps/photos/lib/services/search_service.dart @@ -44,6 +44,7 @@ import "package:photos/models/search/search_types.dart"; import "package:photos/service_locator.dart"; import "package:photos/services/account/user_service.dart"; import 'package:photos/services/collections_service.dart'; +import "package:photos/services/date_parse_service.dart"; import "package:photos/services/filter/db_filters.dart"; import "package:photos/services/location_service.dart"; import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; @@ -60,7 +61,6 @@ import "package:photos/utils/cache_util.dart"; import "package:photos/utils/file_util.dart"; import "package:photos/utils/navigation_util.dart"; import 'package:photos/utils/standalone/date_time.dart'; -import 'package:tuple/tuple.dart'; class SearchService { Future>? _cachedFilesFuture; @@ -1172,20 +1172,56 @@ class SearchService { String query, ) async { final List searchResults = []; - final potentialDates = _getPossibleEventDate(context, query); - for (var potentialDate in potentialDates) { - final int day = potentialDate.item1; - final int month = potentialDate.item2.monthNumber; - final int? year = potentialDate.item3; // nullable - final matchedFiles = - await FilesDB.instance.getFilesCreatedWithinDurations( - _getDurationsForCalendarDateInEveryYear(day, month, year: year), - ignoreCollections(), - order: 'DESC', - ); - if (matchedFiles.isNotEmpty) { - final name = '$day ${potentialDate.item2.name} ${year ?? ''}'; + final dateVariations = DateParseService.instance.parseDateVariations(query); + + // Handle month-year queries + if (DateParseService.instance.isMonthYearQuery(query)) { + final monthYearResult = DateParseService.instance.parseDate(query); + if (monthYearResult.item2 != null && monthYearResult.item3 != null) { + final month = monthYearResult.item2!.monthNumber; + final year = monthYearResult.item3!; + final monthYearFiles = + await FilesDB.instance.getFilesCreatedWithinDurations( + [_getDurationForMonthInYear(month, year)], + ignoreCollections(), + order: 'DESC', + ); + if (monthYearFiles.isNotEmpty) { + final name = '${monthYearResult.item2!.name} $year'; + searchResults.add( + GenericSearchResult( + ResultType.month, + name, + monthYearFiles, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.month, + matchedUploadedIDs: filesToUploadedFileIDs(monthYearFiles), + filterIcon: Icons.calendar_month_outlined, + ), + ), + ); + } + } + } + + for (final dateVar in dateVariations) { + if (dateVar.item1 != null && dateVar.item2 != null) { + final int day = dateVar.item1!; + final int month = dateVar.item2!.monthNumber; + final int? year = dateVar.item3; // nullable for generic dates + + final matchedFiles = + await FilesDB.instance.getFilesCreatedWithinDurations( + _getDurationsForCalendarDateInEveryYear(day, month, year: year), + ignoreCollections(), + order: 'DESC', + ); + + final name = + '$day ${dateVar.item2!.name}${year != null ? ' $year' : ''}'; searchResults.add( GenericSearchResult( ResultType.event, @@ -1641,55 +1677,12 @@ class SearchService { return durationsOfMonthInEveryYear; } - List> _getPossibleEventDate( - BuildContext context, - String query, - ) { - final List> possibleEvents = []; - if (query.trim().isEmpty) { - return possibleEvents; - } - final result = query - .trim() - .split(RegExp('[ ,-/]+')) - .map((e) => e.trim()) - .where((e) => e.isNotEmpty) - .toList(); - final resultCount = result.length; - if (resultCount < 1 || resultCount > 4) { - return possibleEvents; - } - - final int? day = int.tryParse(result[0]); - if (day == null || day < 1 || day > 31) { - return possibleEvents; - } - final List potentialMonth = resultCount > 1 - ? _getMatchingMonths(context, result[1]) - : getMonthData(context); - final int? parsedYear = resultCount >= 3 ? int.tryParse(result[2]) : null; - final List matchingYears = []; - if (parsedYear != null) { - bool foundMatch = false; - for (int i = searchStartYear; i <= currentYear; i++) { - if (i.toString().startsWith(parsedYear.toString())) { - matchingYears.add(i); - foundMatch = foundMatch || (i == parsedYear); - } - } - if (!foundMatch && parsedYear > 1000 && parsedYear <= currentYear) { - matchingYears.add(parsedYear); - } - } - for (var element in potentialMonth) { - if (matchingYears.isEmpty) { - possibleEvents.add(Tuple3(day, element, null)); - } else { - for (int yr in matchingYears) { - possibleEvents.add(Tuple3(day, element, yr)); - } - } - } - return possibleEvents; + List _getDurationForMonthInYear(int month, int year) { + return [ + DateTime(year, month, 1).microsecondsSinceEpoch, + month == 12 + ? DateTime(year + 1, 1, 1).microsecondsSinceEpoch + : DateTime(year, month + 1, 1).microsecondsSinceEpoch, + ]; } } From be6f596b7938919a123065a7d90a74988b5653fc Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Mon, 14 Jul 2025 15:02:58 +0530 Subject: [PATCH 02/29] Minor improvements around date search parsing --- .../lib/services/date_parse_service.dart | 78 ++++++++++--------- .../photos/lib/services/search_service.dart | 53 ++++++------- 2 files changed, 70 insertions(+), 61 deletions(-) diff --git a/mobile/apps/photos/lib/services/date_parse_service.dart b/mobile/apps/photos/lib/services/date_parse_service.dart index 01cd898f19..f70bafe2ea 100644 --- a/mobile/apps/photos/lib/services/date_parse_service.dart +++ b/mobile/apps/photos/lib/services/date_parse_service.dart @@ -7,13 +7,40 @@ class DateParseService { DateParseService._privateConstructor(); static const Map _monthMap = { - "january": 1, "february": 2, "march": 3, "april": 4, - "may": 5, "june": 6, "july": 7, "august": 8, - "september": 9, "october": 10, "november": 11, "december": 12, - "jan": 1, "feb": 2, "mar": 3, "apr": 4, "jun": 6, - "jul": 7, "aug": 8, "sep": 9, "sept": 9, "oct": 10, "nov": 11, "dec": 12, - "janu": 1, "febr": 2, "marc": 3, "apri": 4, "juli": 7, - "augu": 8, "sepe": 9, "octo": 10, "nove": 11, "dece": 12, + "january": 1, + "february": 2, + "march": 3, + "april": 4, + "may": 5, + "june": 6, + "july": 7, + "august": 8, + "september": 9, + "october": 10, + "november": 11, + "december": 12, + "jan": 1, + "feb": 2, + "mar": 3, + "apr": 4, + "jun": 6, + "jul": 7, + "aug": 8, + "sep": 9, + "sept": 9, + "oct": 10, + "nov": 11, + "dec": 12, + "janu": 1, + "febr": 2, + "marc": 3, + "apri": 4, + "juli": 7, + "augu": 8, + "sepe": 9, + "octo": 10, + "nove": 11, + "dece": 12, }; static const Map monthNumberToName = { @@ -74,11 +101,11 @@ class DateParseService { } // Heuristic: if first number > 12, assume DD/MM format - if (first > 12) { + if (first > 12) { if (second > 12) return const Tuple3(null, null, null); return Tuple3(first, second, year); - } else { - if (first > 12) return const Tuple3(null, null, null); + } else { + if (second > 12) return const Tuple3(null, null, null); return Tuple3(second, first, year); } } @@ -105,7 +132,7 @@ class DateParseService { if (second > 12) return const Tuple3(null, null, null); return Tuple3(first, second, year); } else { - if (first > 12) return const Tuple3(null, null, null); + if (second > 12) return const Tuple3(null, null, null); return Tuple3(second, first, year); } } @@ -178,11 +205,11 @@ class DateParseService { return const Tuple3(null, null, null); } - if (first > 12) { + if (first > 12) { if (second > 12) return const Tuple3(null, null, null); return Tuple3(first, second, null); - } else { - if (first > 12) return const Tuple3(null, null, null); + } else { + if (second > 12) return const Tuple3(null, null, null); return Tuple3(second, first, null); } } @@ -334,23 +361,9 @@ class DateParseService { final primaryResult = _parseDateParts(input); variations.add(primaryResult); - - if (primaryResult.item1 != null && - primaryResult.item2 != null && - primaryResult.item3 != null) { - final genericVersion = Tuple3( - primaryResult.item1, - primaryResult.item2, - null, - ); - if (!variations.contains(genericVersion)) { - variations.add(genericVersion); - } - } - return variations; } - + bool isYearQuery(String input) { final yearOnlyMatch = RegExp(r'^\s*(\d{4})\s*$').firstMatch(input.trim()); if (yearOnlyMatch != null) { @@ -359,12 +372,7 @@ class DateParseService { } return false; } - - bool isMonthYearQuery(String input) { - final result = _parseDateParts(input); - return result.item1 == null && result.item2 != null && result.item3 != null; - } - + bool isGenericDateQuery(String input) { final result = _parseDateParts(input); return result.item1 != null && result.item2 != null && result.item3 == null; diff --git a/mobile/apps/photos/lib/services/search_service.dart b/mobile/apps/photos/lib/services/search_service.dart index 8c77d10d8e..f09007c6ee 100644 --- a/mobile/apps/photos/lib/services/search_service.dart +++ b/mobile/apps/photos/lib/services/search_service.dart @@ -1175,12 +1175,13 @@ class SearchService { final dateVariations = DateParseService.instance.parseDateVariations(query); - // Handle month-year queries - if (DateParseService.instance.isMonthYearQuery(query)) { - final monthYearResult = DateParseService.instance.parseDate(query); - if (monthYearResult.item2 != null && monthYearResult.item3 != null) { - final month = monthYearResult.item2!.monthNumber; - final year = monthYearResult.item3!; + for (final dateVar in dateVariations) { + // Handle month-year queries + if (dateVar.item1 == null && + dateVar.item2 != null && + dateVar.item3 != null) { + final month = dateVar.item2!.monthNumber; + final year = dateVar.item3!; final monthYearFiles = await FilesDB.instance.getFilesCreatedWithinDurations( [_getDurationForMonthInYear(month, year)], @@ -1188,7 +1189,7 @@ class SearchService { order: 'DESC', ); if (monthYearFiles.isNotEmpty) { - final name = '${monthYearResult.item2!.name} $year'; + final name = '${dateVar.item2!.name} $year'; searchResults.add( GenericSearchResult( ResultType.month, @@ -1205,10 +1206,8 @@ class SearchService { ); } } - } - - for (final dateVar in dateVariations) { - if (dateVar.item1 != null && dateVar.item2 != null) { + // Handle day-month queries (with or without year) + else if (dateVar.item1 != null && dateVar.item2 != null) { final int day = dateVar.item1!; final int month = dateVar.item2!.monthNumber; final int? year = dateVar.item3; // nullable for generic dates @@ -1220,22 +1219,24 @@ class SearchService { order: 'DESC', ); - final name = - '$day ${dateVar.item2!.name}${year != null ? ' $year' : ''}'; - searchResults.add( - GenericSearchResult( - ResultType.event, - name, - matchedFiles, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(matchedFiles), - filterIcon: Icons.event_outlined, + if (matchedFiles.isNotEmpty) { + final name = + '$day ${dateVar.item2!.name}${year != null ? ' $year' : ''}'; + searchResults.add( + GenericSearchResult( + ResultType.event, + name, + matchedFiles, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.event, + matchedUploadedIDs: filesToUploadedFileIDs(matchedFiles), + filterIcon: Icons.event_outlined, + ), ), - ), - ); + ); + } } } return searchResults; From 209291e09a5fce3d424737447b6fa3f529ba1f61 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 23 Jul 2025 10:32:58 +0200 Subject: [PATCH 03/29] Rename isolate components for clarity --- .../face_clustering_service.dart | 7 +- .../face_thumbnail_generator.dart | 4 +- .../machine_learning/ml_computer.dart | 4 +- .../machine_learning/ml_indexing_isolate.dart | 4 +- .../isolate/isolate_operations.dart} | 0 .../isolate/super_isolate.dart} | 2 +- mobile/apps/photos/pubspec.lock | 114 +++++++++--------- 7 files changed, 68 insertions(+), 67 deletions(-) rename mobile/apps/photos/lib/{services/isolate_functions.dart => utils/isolate/isolate_operations.dart} (100%) rename mobile/apps/photos/lib/{services/isolate_service.dart => utils/isolate/super_isolate.dart} (99%) diff --git a/mobile/apps/photos/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart b/mobile/apps/photos/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart index 2e79215cf1..b11ff67fb5 100644 --- a/mobile/apps/photos/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart +++ b/mobile/apps/photos/lib/services/machine_learning/face_ml/face_clustering/face_clustering_service.dart @@ -8,11 +8,11 @@ import "package:ml_linalg/dtype.dart"; import "package:ml_linalg/vector.dart"; import "package:photos/generated/protos/ente/common/vector.pb.dart"; import "package:photos/models/base/id.dart"; -import "package:photos/services/isolate_functions.dart"; -import "package:photos/services/isolate_service.dart"; import "package:photos/services/machine_learning/face_ml/face_clustering/face_db_info_for_clustering.dart"; import "package:photos/services/machine_learning/face_ml/face_filtering/face_filtering_constants.dart"; import "package:photos/services/machine_learning/ml_result.dart"; +import "package:photos/utils/isolate/isolate_operations.dart"; +import "package:photos/utils/isolate/super_isolate.dart"; class FaceInfo { final String faceID; @@ -507,7 +507,8 @@ ClusteringResult _runCompleteClustering(Map args) { EVector.fromBuffer(entry.value).values, dtype: DType.float32, ), - fileCreationTime: fileIDToCreationTime?[getFileIdFromFaceId(entry.key)], + fileCreationTime: + fileIDToCreationTime?[getFileIdFromFaceId(entry.key)], ), ); } diff --git a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart index 4980f2e904..a51541863c 100644 --- a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart +++ b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart @@ -4,9 +4,9 @@ import 'dart:typed_data' show Uint8List; import "package:computer/computer.dart"; import "package:logging/logging.dart"; import "package:photos/models/ml/face/box.dart"; -import "package:photos/services/isolate_functions.dart"; -import "package:photos/services/isolate_service.dart"; import "package:photos/utils/image_ml_util.dart"; +import "package:photos/utils/isolate/isolate_operations.dart"; +import "package:photos/utils/isolate/super_isolate.dart"; final Computer _computer = Computer.shared(); diff --git a/mobile/apps/photos/lib/services/machine_learning/ml_computer.dart b/mobile/apps/photos/lib/services/machine_learning/ml_computer.dart index edbc16b7ff..939d486f64 100644 --- a/mobile/apps/photos/lib/services/machine_learning/ml_computer.dart +++ b/mobile/apps/photos/lib/services/machine_learning/ml_computer.dart @@ -2,12 +2,12 @@ import 'dart:async'; import "package:logging/logging.dart"; import "package:photos/models/ml/vector.dart"; -import "package:photos/services/isolate_functions.dart"; -import "package:photos/services/isolate_service.dart"; import "package:photos/services/machine_learning/ml_constants.dart"; import "package:photos/services/machine_learning/semantic_search/clip/clip_text_encoder.dart"; import "package:photos/services/machine_learning/semantic_search/query_result.dart"; import "package:photos/services/remote_assets_service.dart"; +import "package:photos/utils/isolate/isolate_operations.dart"; +import "package:photos/utils/isolate/super_isolate.dart"; import "package:synchronized/synchronized.dart"; class MLComputer extends SuperIsolate { diff --git a/mobile/apps/photos/lib/services/machine_learning/ml_indexing_isolate.dart b/mobile/apps/photos/lib/services/machine_learning/ml_indexing_isolate.dart index e66848bb27..b386674b31 100644 --- a/mobile/apps/photos/lib/services/machine_learning/ml_indexing_isolate.dart +++ b/mobile/apps/photos/lib/services/machine_learning/ml_indexing_isolate.dart @@ -2,14 +2,14 @@ import "dart:async"; import "package:flutter/foundation.dart" show debugPrint; import "package:logging/logging.dart"; -import "package:photos/services/isolate_functions.dart"; -import "package:photos/services/isolate_service.dart"; import 'package:photos/services/machine_learning/face_ml/face_detection/face_detection_service.dart'; import 'package:photos/services/machine_learning/face_ml/face_embedding/face_embedding_service.dart'; import "package:photos/services/machine_learning/ml_models_overview.dart"; import 'package:photos/services/machine_learning/ml_result.dart'; import "package:photos/services/machine_learning/semantic_search/clip/clip_image_encoder.dart"; import "package:photos/services/remote_assets_service.dart"; +import "package:photos/utils/isolate/isolate_operations.dart"; +import "package:photos/utils/isolate/super_isolate.dart"; import "package:photos/utils/ml_util.dart"; import "package:photos/utils/network_util.dart"; import "package:synchronized/synchronized.dart"; diff --git a/mobile/apps/photos/lib/services/isolate_functions.dart b/mobile/apps/photos/lib/utils/isolate/isolate_operations.dart similarity index 100% rename from mobile/apps/photos/lib/services/isolate_functions.dart rename to mobile/apps/photos/lib/utils/isolate/isolate_operations.dart diff --git a/mobile/apps/photos/lib/services/isolate_service.dart b/mobile/apps/photos/lib/utils/isolate/super_isolate.dart similarity index 99% rename from mobile/apps/photos/lib/services/isolate_service.dart rename to mobile/apps/photos/lib/utils/isolate/super_isolate.dart index 4a91c64473..f37c2d4b3b 100644 --- a/mobile/apps/photos/lib/services/isolate_service.dart +++ b/mobile/apps/photos/lib/utils/isolate/super_isolate.dart @@ -7,7 +7,7 @@ import "package:flutter/services.dart"; import "package:logging/logging.dart"; import "package:photos/core/error-reporting/isolate_logging.dart"; import "package:photos/models/base/id.dart"; -import "package:photos/services/isolate_functions.dart"; +import "package:photos/utils/isolate/isolate_operations.dart"; import "package:synchronized/synchronized.dart"; abstract class SuperIsolate { diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock index 58284d5f09..a5d14c3829 100644 --- a/mobile/apps/photos/pubspec.lock +++ b/mobile/apps/photos/pubspec.lock @@ -5,10 +5,10 @@ packages: dependency: transitive description: name: _fe_analyzer_shared - sha256: f256b0c0ba6c7577c15e2e4e114755640a875e885099367bf6e012b19314c834 + sha256: "16e298750b6d0af7ce8a3ba7c18c69c3785d11b15ec83f6dcd0ad2a0009b3cab" url: "https://pub.dev" source: hosted - version: "72.0.0" + version: "76.0.0" _flutterfire_internals: dependency: transitive description: @@ -21,7 +21,7 @@ packages: dependency: transitive description: dart source: sdk - version: "0.3.2" + version: "0.3.3" adaptive_theme: dependency: "direct main" description: @@ -34,10 +34,10 @@ packages: dependency: transitive description: name: analyzer - sha256: b652861553cd3990d8ed361f7979dc6d7053a9ac8843fa73820ab68ce5410139 + sha256: "1f14db053a8c23e260789e9b0980fa27f2680dd640932cae5e1137cce0e46e1e" url: "https://pub.dev" source: hosted - version: "6.7.0" + version: "6.11.0" android_intent_plus: dependency: "direct main" description: @@ -130,10 +130,10 @@ packages: dependency: "direct main" description: name: async - sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" + sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 url: "https://pub.dev" source: hosted - version: "2.11.0" + version: "2.12.0" battery_info: dependency: "direct main" description: @@ -155,10 +155,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" url: "https://pub.dev" source: hosted - version: "2.1.1" + version: "2.1.2" brotli: dependency: transitive description: @@ -268,10 +268,10 @@ packages: dependency: transitive description: name: characters - sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 url: "https://pub.dev" source: hosted - version: "1.3.0" + version: "1.4.0" checked_yaml: dependency: transitive description: @@ -301,10 +301,10 @@ packages: dependency: transitive description: name: clock - sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b url: "https://pub.dev" source: hosted - version: "1.1.1" + version: "1.1.2" code_builder: dependency: transitive description: @@ -317,10 +317,10 @@ packages: dependency: "direct main" description: name: collection - sha256: ee67cb0715911d28db6bf4af1026078bd6f0128b07a5f66fb2ed94ec6783c09a + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" url: "https://pub.dev" source: hosted - version: "1.18.0" + version: "1.19.1" computer: dependency: "direct main" description: @@ -619,10 +619,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" + sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" url: "https://pub.dev" source: hosted - version: "1.3.1" + version: "1.3.2" fast_base58: dependency: "direct main" description: @@ -668,10 +668,10 @@ packages: dependency: transitive description: name: file - sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 url: "https://pub.dev" source: hosted - version: "7.0.0" + version: "7.0.1" file_saver: dependency: "direct main" description: @@ -1416,18 +1416,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" + sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec url: "https://pub.dev" source: hosted - version: "10.0.5" + version: "10.0.8" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" + sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 url: "https://pub.dev" source: hosted - version: "3.0.5" + version: "3.0.9" leak_tracker_testing: dependency: transitive description: @@ -1536,10 +1536,10 @@ packages: dependency: transitive description: name: macros - sha256: "0acaed5d6b7eab89f63350bccd82119e6c602df0f391260d0e32b5e23db79536" + sha256: "1d9e801cd66f7ea3663c45fc708450db1fa57f988142c64289142c9b7ee80656" url: "https://pub.dev" source: hosted - version: "0.1.2-main.4" + version: "0.1.3-main.0" maps_launcher: dependency: "direct main" description: @@ -1552,10 +1552,10 @@ packages: dependency: transitive description: name: matcher - sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 url: "https://pub.dev" source: hosted - version: "0.12.16+1" + version: "0.12.17" material_color_utilities: dependency: transitive description: @@ -1645,10 +1645,10 @@ packages: dependency: transitive description: name: meta - sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c url: "https://pub.dev" source: hosted - version: "1.15.0" + version: "1.16.0" mgrs_dart: dependency: transitive description: @@ -1859,10 +1859,10 @@ packages: dependency: "direct main" description: name: path - sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" url: "https://pub.dev" source: hosted - version: "1.9.0" + version: "1.9.1" path_drawing: dependency: transitive description: @@ -2019,10 +2019,10 @@ packages: dependency: transitive description: name: platform - sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" url: "https://pub.dev" source: hosted - version: "3.1.5" + version: "3.1.6" plugin_platform_interface: dependency: transitive description: @@ -2068,10 +2068,10 @@ packages: dependency: transitive description: name: process - sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" + sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" url: "https://pub.dev" source: hosted - version: "5.0.2" + version: "5.0.3" proj4dart: dependency: transitive description: @@ -2309,7 +2309,7 @@ packages: dependency: transitive description: flutter source: sdk - version: "0.0.99" + version: "0.0.0" source_gen: dependency: transitive description: @@ -2346,10 +2346,10 @@ packages: dependency: transitive description: name: source_span - sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" url: "https://pub.dev" source: hosted - version: "1.10.0" + version: "1.10.1" sprintf: dependency: transitive description: @@ -2434,10 +2434,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "73713990125a6d93122541237550ee3352a2d84baad52d375a4cad2eb9b7ce0b" + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" url: "https://pub.dev" source: hosted - version: "1.11.1" + version: "1.12.1" step_progress_indicator: dependency: "direct main" description: @@ -2450,10 +2450,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.4" stream_transform: dependency: transitive description: @@ -2466,10 +2466,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" url: "https://pub.dev" source: hosted - version: "1.2.0" + version: "1.4.1" styled_text: dependency: "direct main" description: @@ -2522,34 +2522,34 @@ packages: dependency: transitive description: name: term_glyph - sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" url: "https://pub.dev" source: hosted - version: "1.2.1" + version: "1.2.2" test: dependency: "direct dev" description: name: test - sha256: "7ee44229615f8f642b68120165ae4c2a75fe77ae2065b1e55ae4711f6cf0899e" + sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" url: "https://pub.dev" source: hosted - version: "1.25.7" + version: "1.25.15" test_api: dependency: transitive description: name: test_api - sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" + sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd url: "https://pub.dev" source: hosted - version: "0.7.2" + version: "0.7.4" test_core: dependency: transitive description: name: test_core - sha256: "55ea5a652e38a1dfb32943a7973f3681a60f872f8c3a05a14664ad54ef9c6696" + sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" url: "https://pub.dev" source: hosted - version: "0.6.4" + version: "0.6.8" thermal: dependency: "direct main" description: @@ -2813,10 +2813,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" + sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" url: "https://pub.dev" source: hosted - version: "14.2.5" + version: "14.3.1" volume_controller: dependency: transitive description: @@ -2877,10 +2877,10 @@ packages: dependency: transitive description: name: webdriver - sha256: "003d7da9519e1e5f329422b36c4dcdf18d7d2978d1ba099ea4e45ba490ed845e" + sha256: "3d773670966f02a646319410766d3b5e1037efb7f07cc68f844d5e06cd4d61c8" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.4" webkit_inspection_protocol: dependency: transitive description: @@ -2978,5 +2978,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.5.0 <4.0.0" + dart: ">=3.7.0-0 <4.0.0" flutter: ">=3.24.0" From 4260c3c7690d141addfdca0543e6a35e00c6e294 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 23 Jul 2025 10:33:31 +0200 Subject: [PATCH 04/29] Remove redundant code --- .../services/machine_learning/face_thumbnail_generator.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart index a51541863c..60de96d1df 100644 --- a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart +++ b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart @@ -1,15 +1,12 @@ import 'dart:async'; import 'dart:typed_data' show Uint8List; -import "package:computer/computer.dart"; import "package:logging/logging.dart"; import "package:photos/models/ml/face/box.dart"; import "package:photos/utils/image_ml_util.dart"; import "package:photos/utils/isolate/isolate_operations.dart"; import "package:photos/utils/isolate/super_isolate.dart"; -final Computer _computer = Computer.shared(); - class FaceThumbnailGenerator extends SuperIsolate { @override Logger get logger => _logger; From 1cc3499019449bf02f6342214b19f423e01466d4 Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 23 Jul 2025 10:34:19 +0200 Subject: [PATCH 05/29] face thumbnail fix pragma entry point --- .../lib/services/machine_learning/face_thumbnail_generator.dart | 1 + mobile/apps/photos/lib/utils/isolate/super_isolate.dart | 1 + 2 files changed, 2 insertions(+) diff --git a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart index 60de96d1df..b61763c782 100644 --- a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart +++ b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart @@ -7,6 +7,7 @@ import "package:photos/utils/image_ml_util.dart"; import "package:photos/utils/isolate/isolate_operations.dart"; import "package:photos/utils/isolate/super_isolate.dart"; +@pragma('vm:entry-point') class FaceThumbnailGenerator extends SuperIsolate { @override Logger get logger => _logger; diff --git a/mobile/apps/photos/lib/utils/isolate/super_isolate.dart b/mobile/apps/photos/lib/utils/isolate/super_isolate.dart index f37c2d4b3b..a2e509fabc 100644 --- a/mobile/apps/photos/lib/utils/isolate/super_isolate.dart +++ b/mobile/apps/photos/lib/utils/isolate/super_isolate.dart @@ -10,6 +10,7 @@ import "package:photos/models/base/id.dart"; import "package:photos/utils/isolate/isolate_operations.dart"; import "package:synchronized/synchronized.dart"; +@pragma('vm:entry-point') abstract class SuperIsolate { Logger get logger; From b16c9af36bf322a433285ab4b314e0074c47872e Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 23 Jul 2025 10:47:01 +0200 Subject: [PATCH 06/29] Logging in super isolate when starting operation --- mobile/apps/photos/lib/utils/isolate/super_isolate.dart | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mobile/apps/photos/lib/utils/isolate/super_isolate.dart b/mobile/apps/photos/lib/utils/isolate/super_isolate.dart index a2e509fabc..3a83eaef1b 100644 --- a/mobile/apps/photos/lib/utils/isolate/super_isolate.dart +++ b/mobile/apps/photos/lib/utils/isolate/super_isolate.dart @@ -81,6 +81,8 @@ abstract class SuperIsolate { if (rootToken != null) { BackgroundIsolateBinaryMessenger.ensureInitialized(rootToken); } + final logger = Logger('SuperIsolate'); + logger.info('IsolateMain started'); receivePort.listen((message) async { final taskID = message[0] as String; @@ -88,6 +90,7 @@ abstract class SuperIsolate { final function = IsolateOperation.values[functionIndex]; final args = message[2] as Map; final sendPort = message[3] as SendPort; + logger.info("Starting isolate operation $function in isolate"); late final Object data; try { From 1718e5d1d6d657eb1ac54d8d644f1145bfe61ccc Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 23 Jul 2025 11:33:30 +0200 Subject: [PATCH 07/29] More careful logging --- mobile/apps/photos/lib/db/ml/db.dart | 12 ++++++ .../face_thumbnail_generator.dart | 40 ++++++++++++------- .../ui/viewer/people/person_face_widget.dart | 12 ++++-- .../lib/utils/face/face_thumbnail_cache.dart | 6 ++- .../apps/photos/lib/utils/image_ml_util.dart | 2 +- 5 files changed, 51 insertions(+), 21 deletions(-) diff --git a/mobile/apps/photos/lib/db/ml/db.dart b/mobile/apps/photos/lib/db/ml/db.dart index 851e349d00..5ab5d28c8b 100644 --- a/mobile/apps/photos/lib/db/ml/db.dart +++ b/mobile/apps/photos/lib/db/ml/db.dart @@ -360,6 +360,10 @@ class MLDataDB with SqlDbBase implements IMLDataDB { } return mapRowToFace(faceMaps.first); } + } else if (clusterID == null) { + _logger.severe( + "Didn't find any faces for personID $personID in `getCoverFaceForPerson`.", + ); } if (clusterID != null) { const String queryFaceID = ''' @@ -380,11 +384,19 @@ class MLDataDB with SqlDbBase implements IMLDataDB { return face; } } + } else { + _logger.severe( + "Didn't find any faces for clusterID $clusterID in `getCoverFaceForPerson`. faces: $faces", + ); } } if (personID == null && clusterID == null) { + _logger.severe("personID and clusterID cannot be null both"); throw Exception("personID and clusterID cannot be null"); } + _logger.severe( + "Something went wrong finding a face from `getCoverFaceForPerson` (personID: $personID, clusterID: $clusterID)", + ); return null; } diff --git a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart index b61763c782..6e09744cff 100644 --- a/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart +++ b/mobile/apps/photos/lib/services/machine_learning/face_thumbnail_generator.dart @@ -35,20 +35,30 @@ class FaceThumbnailGenerator extends SuperIsolate { String imagePath, List faceBoxes, ) async { - final List> faceBoxesJson = - faceBoxes.map((box) => box.toJson()).toList(); - final List faces = await runInIsolate( - IsolateOperation.generateFaceThumbnails, - { - 'imagePath': imagePath, - 'faceBoxesList': faceBoxesJson, - }, - ).then((value) => value.cast()); - final compressedFaces = - await compressFaceThumbnails({'listPngBytes': faces}); - _logger.fine( - "Compressed face thumbnails from sizes ${faces.map((e) => e.length / 1024).toList()} to ${compressedFaces.map((e) => e.length / 1024).toList()} kilobytes", - ); - return compressedFaces; + try { + _logger.info( + "Generating face thumbnails for ${faceBoxes.length} face boxes in $imagePath", + ); + final List> faceBoxesJson = + faceBoxes.map((box) => box.toJson()).toList(); + final List faces = await runInIsolate( + IsolateOperation.generateFaceThumbnails, + { + 'imagePath': imagePath, + 'faceBoxesList': faceBoxesJson, + }, + ).then((value) => value.cast()); + _logger.info("Generated face thumbnails"); + final compressedFaces = + await compressFaceThumbnails({'listPngBytes': faces}); + _logger.fine( + "Compressed face thumbnails from sizes ${faces.map((e) => e.length / 1024).toList()} to ${compressedFaces.map((e) => e.length / 1024).toList()} kilobytes", + ); + return compressedFaces; + } catch (e, s) { + _logger.severe("Failed to generate face thumbnails", e, s); + + rethrow; + } } } diff --git a/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart b/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart index 8bd069f57e..0327c5853d 100644 --- a/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart +++ b/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart @@ -163,7 +163,7 @@ class _PersonFaceWidgetState extends State { } } if (fileForFaceCrop == null) { - _logger.warning( + _logger.severe( "No suitable file found for face crop for person: ${widget.personId} or cluster: ${widget.clusterID}", ); return null; @@ -176,7 +176,7 @@ class _PersonFaceWidgetState extends State { clusterID: widget.clusterID, ); if (face == null) { - debugPrint( + _logger.severe( "No cover face for person: ${widget.personId} or cluster ${widget.clusterID} and fileID ${fileForFaceCrop.uploadedFileID!}", ); return null; @@ -188,7 +188,13 @@ class _PersonFaceWidgetState extends State { personOrClusterID: personOrClusterId, useTempCache: false, ); - return cropMap?[face.faceID]; + final result = cropMap?[face.faceID]; + if (result == null) { + _logger.severe( + "Null cover face crop for person: ${widget.personId} or cluster ${widget.clusterID} and fileID ${fileForFaceCrop.uploadedFileID!}", + ); + } + return result; } catch (e, s) { _logger.severe( "Error getting cover face for person: ${widget.personId} or cluster ${widget.clusterID}", diff --git a/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart b/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart index 117793173f..448eb7789a 100644 --- a/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart +++ b/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart @@ -129,7 +129,7 @@ Future?> getCachedFaceCrops( ); faceIdToCrop[face.faceID] = data; } else { - _logger.warning( + _logger.severe( "Cached face crop for faceID ${face.faceID} is empty, deleting file ${faceCropCacheFile.path}", ); await faceCropCacheFile.delete(); @@ -231,7 +231,7 @@ Future?> getCachedFaceCrops( s, ); } else { - _logger.info( + _logger.severe( "Stopped getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()} due to $e", ); } @@ -334,12 +334,14 @@ Future?> _getFaceCrops( if (useFullFile && file.fileType != FileType.video) { final File? ioFile = await getFile(file); if (ioFile == null) { + _logger.severe("Failed to get file for face crop generation"); return null; } imagePath = ioFile.path; } else { final thumbnail = await getThumbnailForUploadedFile(file); if (thumbnail == null) { + _logger.severe("Failed to get thumbnail for face crop generation"); return null; } imagePath = thumbnail.path; diff --git a/mobile/apps/photos/lib/utils/image_ml_util.dart b/mobile/apps/photos/lib/utils/image_ml_util.dart index 0ab1428fea..ab8b4a4e0f 100644 --- a/mobile/apps/photos/lib/utils/image_ml_util.dart +++ b/mobile/apps/photos/lib/utils/image_ml_util.dart @@ -575,7 +575,7 @@ Future> compressFaceThumbnails(Map args) async { } return await Future.wait(compressedBytesList); } catch (e, s) { - _logger.warning( + _logger.severe( 'Failed to compress face thumbnail, using original. Size: ${listPngBytes.map((e) => e.length).toList()} bytes', e, s, From 3bba125f1ccfc6adea534a32f67ca335bc701902 Mon Sep 17 00:00:00 2001 From: max977 Date: Wed, 23 Jul 2025 11:51:25 +0200 Subject: [PATCH 08/29] custom-icon-startmail Adding Custom Icon for Startmail --- .../assets/custom-icons/_data/custom-icons.json | 4 ++++ .../auth/assets/custom-icons/icons/startmail.svg | 15 +++++++++++++++ 2 files changed, 19 insertions(+) create mode 100755 mobile/apps/auth/assets/custom-icons/icons/startmail.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index 129e14ca96..ba42bb47f1 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -1295,6 +1295,10 @@ "PAYDAY 3" ] }, + { + "title": "Startmail", + "slug": "startmail" + }, { "title": "STRATO", "hex": "FF8800" diff --git a/mobile/apps/auth/assets/custom-icons/icons/startmail.svg b/mobile/apps/auth/assets/custom-icons/icons/startmail.svg new file mode 100755 index 0000000000..3df4bc496b --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/startmail.svg @@ -0,0 +1,15 @@ + + + + + + + + + From a06a5be98382b7034b1fd5f37267e18a5ed6ddf7 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 23 Jul 2025 15:45:20 +0530 Subject: [PATCH 09/29] [mob] Skip dup fileID from src collection during copy --- .../lib/services/collections_service.dart | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/mobile/apps/photos/lib/services/collections_service.dart b/mobile/apps/photos/lib/services/collections_service.dart index ee3d662a63..a8c1986f1b 100644 --- a/mobile/apps/photos/lib/services/collections_service.dart +++ b/mobile/apps/photos/lib/services/collections_service.dart @@ -1427,12 +1427,20 @@ class CollectionsService { } // group files by collectionID final Map> filesByCollection = {}; + final Map> fileSeenByCollection = {}; for (final file in filesToCopy) { - if (filesByCollection.containsKey(file.collectionID!)) { - filesByCollection[file.collectionID!]!.add(file.copyWith()); - } else { - filesByCollection[file.collectionID!] = [file.copyWith()]; + fileSeenByCollection.putIfAbsent(file.collectionID!, () => {}); + if (fileSeenByCollection[file.collectionID]! + .contains(file.uploadedFileID)) { + _logger.warning( + "skip copy, duplicate ID: ${file.uploadedFileID} in collection " + "${file.collectionID}", + ); + continue; } + filesByCollection + .putIfAbsent(file.collectionID!, () => []) + .add(file.copyWith()); } for (final entry in filesByCollection.entries) { final srcCollectionID = entry.key; @@ -1579,9 +1587,6 @@ class CollectionsService { params["files"] = []; for (final batchFile in batch) { final fileKey = getFileKey(batchFile); - _logger.info( - "srcCollection : $srcCollectionID file: ${batchFile.uploadedFileID} key: ${CryptoUtil.bin2base64(fileKey)} ", - ); final encryptedKeyData = CryptoUtil.encryptSync(fileKey, getCollectionKey(dstCollectionID)); batchFile.encryptedKey = From c2d1c668887ec5baca472d00f520c2cce24387ba Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 23 Jul 2025 12:45:09 +0200 Subject: [PATCH 10/29] keep alive face thumbnail when scrolling fast --- .../ui/viewer/people/person_face_widget.dart | 12 +++++++++++- .../search/result/people_section_all_page.dart | 17 +++++++++++++++-- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart b/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart index 0327c5853d..3e1b138c26 100644 --- a/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart +++ b/mobile/apps/photos/lib/ui/viewer/people/person_face_widget.dart @@ -21,6 +21,7 @@ class PersonFaceWidget extends StatefulWidget { final String? clusterID; final bool useFullFile; final VoidCallback? onErrorCallback; + final bool keepAlive; // PersonFaceWidget constructor checks that both personId and clusterID are not null // and that the file is not null @@ -29,6 +30,7 @@ class PersonFaceWidget extends StatefulWidget { this.clusterID, this.useFullFile = true, this.onErrorCallback, + this.keepAlive = false, super.key, }) : assert( personId != null || clusterID != null, @@ -39,12 +41,16 @@ class PersonFaceWidget extends StatefulWidget { State createState() => _PersonFaceWidgetState(); } -class _PersonFaceWidgetState extends State { +class _PersonFaceWidgetState extends State + with AutomaticKeepAliveClientMixin { Future? faceCropFuture; EnteFile? fileForFaceCrop; bool get isPerson => widget.personId != null; + @override + bool get wantKeepAlive => widget.keepAlive; + @override void initState() { super.initState(); @@ -64,6 +70,10 @@ class _PersonFaceWidgetState extends State { @override Widget build(BuildContext context) { + super.build( + context, + ); // Calling super.build for AutomaticKeepAliveClientMixin + return FutureBuilder( future: faceCropFuture, builder: (context, snapshot) { diff --git a/mobile/apps/photos/lib/ui/viewer/search/result/people_section_all_page.dart b/mobile/apps/photos/lib/ui/viewer/search/result/people_section_all_page.dart index d78eb4b9ba..d711bd9ebd 100644 --- a/mobile/apps/photos/lib/ui/viewer/search/result/people_section_all_page.dart +++ b/mobile/apps/photos/lib/ui/viewer/search/result/people_section_all_page.dart @@ -95,12 +95,14 @@ class SelectablePersonSearchExample extends StatelessWidget { final GenericSearchResult searchResult; final double size; final SelectedPeople selectedPeople; + final bool isDefaultFace; const SelectablePersonSearchExample({ super.key, required this.searchResult, required this.selectedPeople, this.size = 102, + this.isDefaultFace = false, }); void _handleTap(BuildContext context) { @@ -192,7 +194,10 @@ class SelectablePersonSearchExample extends StatelessWidget { searchResult.previewThumbnail()!, shouldShowSyncStatus: false, ) - : FaceSearchResult(searchResult); + : FaceSearchResult( + searchResult, + isDefaultFace: isDefaultFace, + ); } else { child = const NoThumbnailWidget( addBorder: false, @@ -301,8 +306,13 @@ class SelectablePersonSearchExample extends StatelessWidget { class FaceSearchResult extends StatelessWidget { final SearchResult searchResult; + final bool isDefaultFace; - const FaceSearchResult(this.searchResult, {super.key}); + const FaceSearchResult( + this.searchResult, { + super.key, + this.isDefaultFace = false, + }); @override Widget build(BuildContext context) { @@ -313,6 +323,7 @@ class FaceSearchResult extends StatelessWidget { key: params.containsKey(kPersonWidgetKey) ? ValueKey(params[kPersonWidgetKey]) : ValueKey(params[kPersonParamID] ?? params[kClusterParamId]), + keepAlive: isDefaultFace, ); } } @@ -486,6 +497,7 @@ class _PeopleSectionAllWidgetState extends State { searchResult: normalFaces[index], size: itemSize, selectedPeople: widget.selectedPeople!, + isDefaultFace: true, ) : PersonSearchExample( searchResult: normalFaces[index], @@ -525,6 +537,7 @@ class _PeopleSectionAllWidgetState extends State { searchResult: extraFaces[index], size: itemSize, selectedPeople: widget.selectedPeople!, + isDefaultFace: false, ) : PersonSearchExample( searchResult: extraFaces[index], From 843e956a8aa8f6dd94ba1179aad649f3082eae73 Mon Sep 17 00:00:00 2001 From: Neeraj Date: Wed, 23 Jul 2025 16:45:04 +0530 Subject: [PATCH 11/29] [web] Update download link for auth apps --- web/apps/auth/src/pages/auth.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 8f9e18733b..5c9809c9c6 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -405,7 +405,7 @@ const Footer: React.FC = () => { {t("auth_download_mobile_app")} From 4a743be322daae0ff8d68911c3815121395ed53c Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Wed, 23 Jul 2025 16:41:55 +0530 Subject: [PATCH 12/29] [mob]Handle duplicate fileID during addOrCopy --- .../lib/services/collections_service.dart | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/mobile/apps/photos/lib/services/collections_service.dart b/mobile/apps/photos/lib/services/collections_service.dart index a8c1986f1b..86ed68924b 100644 --- a/mobile/apps/photos/lib/services/collections_service.dart +++ b/mobile/apps/photos/lib/services/collections_service.dart @@ -1648,17 +1648,27 @@ class CollectionsService { ); final List filesToCopy = []; final List filesToAdd = []; + final Set seenForAdd = {}; + final Set seenForCopy = {}; + for (final EnteFile file in othersFile) { - if (hashToUserFile.containsKey(file.hash ?? '')) { - final userFile = hashToUserFile[file.hash]!; - if (userFile.fileType == file.fileType) { - filesToAdd.add(userFile); - } else { - filesToCopy.add(file); - } - } else { - filesToCopy.add(file); + final userFile = hashToUserFile[file.hash ?? '']; + final bool shouldAdd = + userFile != null && userFile.fileType == file.fileType; + final targetList = shouldAdd ? filesToAdd : filesToCopy; + final seenSet = shouldAdd ? seenForAdd : seenForCopy; + final fileToProcess = shouldAdd ? userFile : file; + final uploadID = fileToProcess.uploadedFileID; + + if (seenSet.contains(uploadID)) { + final action = shouldAdd ? "adding" : "copying"; + _logger.warning( + "skip $action file $uploadID as it is already ${action}ed", + ); + continue; } + targetList.add(fileToProcess); + seenSet.add(uploadID!); } return (filesToAdd, filesToCopy); } From 968f04c04af4942b734cdcfd36bf960ee26c954a Mon Sep 17 00:00:00 2001 From: laurenspriem Date: Wed, 23 Jul 2025 13:45:37 +0200 Subject: [PATCH 13/29] Lower severity logging --- mobile/apps/photos/lib/db/ml/db.dart | 8 -------- .../apps/photos/lib/utils/face/face_thumbnail_cache.dart | 8 ++++---- mobile/apps/photos/lib/utils/image_ml_util.dart | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/mobile/apps/photos/lib/db/ml/db.dart b/mobile/apps/photos/lib/db/ml/db.dart index 5ab5d28c8b..bc0913f8fe 100644 --- a/mobile/apps/photos/lib/db/ml/db.dart +++ b/mobile/apps/photos/lib/db/ml/db.dart @@ -360,10 +360,6 @@ class MLDataDB with SqlDbBase implements IMLDataDB { } return mapRowToFace(faceMaps.first); } - } else if (clusterID == null) { - _logger.severe( - "Didn't find any faces for personID $personID in `getCoverFaceForPerson`.", - ); } if (clusterID != null) { const String queryFaceID = ''' @@ -384,10 +380,6 @@ class MLDataDB with SqlDbBase implements IMLDataDB { return face; } } - } else { - _logger.severe( - "Didn't find any faces for clusterID $clusterID in `getCoverFaceForPerson`. faces: $faces", - ); } } if (personID == null && clusterID == null) { diff --git a/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart b/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart index 448eb7789a..6f390fcf77 100644 --- a/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart +++ b/mobile/apps/photos/lib/utils/face/face_thumbnail_cache.dart @@ -129,14 +129,14 @@ Future?> getCachedFaceCrops( ); faceIdToCrop[face.faceID] = data; } else { - _logger.severe( + _logger.warning( "Cached face crop for faceID ${face.faceID} is empty, deleting file ${faceCropCacheFile.path}", ); await faceCropCacheFile.delete(); facesWithoutCrops[face.faceID] = face.detection.box; } } catch (e, s) { - _logger.severe( + _logger.warning( "Error reading cached face crop for faceID ${face.faceID} from file ${faceCropCacheFile.path}", e, s, @@ -212,7 +212,7 @@ Future?> getCachedFaceCrops( milliseconds: 100 * pow(2, fetchAttempt + 1).toInt(), ); await Future.delayed(backoff); - _logger.warning( + _logger.fine( "Error getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()}, retrying (attempt ${fetchAttempt + 1}) in ${backoff.inMilliseconds} ms", e, s, @@ -225,7 +225,7 @@ Future?> getCachedFaceCrops( useTempCache: useTempCache, ); } - _logger.severe( + _logger.warning( "Error getting face crops for faceIDs: ${faces.map((face) => face.faceID).toList()}", e, s, diff --git a/mobile/apps/photos/lib/utils/image_ml_util.dart b/mobile/apps/photos/lib/utils/image_ml_util.dart index ab8b4a4e0f..0ab1428fea 100644 --- a/mobile/apps/photos/lib/utils/image_ml_util.dart +++ b/mobile/apps/photos/lib/utils/image_ml_util.dart @@ -575,7 +575,7 @@ Future> compressFaceThumbnails(Map args) async { } return await Future.wait(compressedBytesList); } catch (e, s) { - _logger.severe( + _logger.warning( 'Failed to compress face thumbnail, using original. Size: ${listPngBytes.map((e) => e.length).toList()} bytes', e, s, From b58aeddeba45728c47fb9233f79c9a2275ea6146 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:27:49 +0530 Subject: [PATCH 14/29] [mob][photos] Remove file entry for files that are already queued --- mobile/apps/photos/lib/db/files_db.dart | 23 +++++++++++++++++++ .../services/sync/remote_sync_service.dart | 3 +++ 2 files changed, 26 insertions(+) diff --git a/mobile/apps/photos/lib/db/files_db.dart b/mobile/apps/photos/lib/db/files_db.dart index f8900f8912..3150dbc3b6 100644 --- a/mobile/apps/photos/lib/db/files_db.dart +++ b/mobile/apps/photos/lib/db/files_db.dart @@ -979,6 +979,29 @@ class FilesDB with SqlDbBase { return result; } + // remove references for local files which are either already uploaded + // or queued for upload but not yet uploaded + Future removeQueuedLocalFiles(Set localIDs) async { + final db = await instance.sqliteAsyncDB; + final inParam = localIDs.map((id) => "'$id'").join(','); + final r = await db.execute( + ''' + DELETE FROM $filesTable + WHERE $columnLocalID IN ($inParam) and (collectionID IS NULL || collectionID = -1) + and ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1); + ''', + ); + if (r.isNotEmpty) { + _logger.warning( + "Removed ${r.length} potential dups for already queued local files", + ); + } else { + _logger.finest("No duplicate id found for queued/uploaded files"); + } + + return r.length; + } + Future> getLocalFileIDsForCollection(int collectionID) async { final db = await instance.sqliteAsyncDB; final rows = await db.getAll( diff --git a/mobile/apps/photos/lib/services/sync/remote_sync_service.dart b/mobile/apps/photos/lib/services/sync/remote_sync_service.dart index d4523b27d0..d0e995a438 100644 --- a/mobile/apps/photos/lib/services/sync/remote_sync_service.dart +++ b/mobile/apps/photos/lib/services/sync/remote_sync_service.dart @@ -371,6 +371,9 @@ class RemoteSyncService { final Set alreadyClaimedLocalIDs = await _db.getLocalIDsMarkedForOrAlreadyUploaded(ownerID); localIDsToSync.removeAll(alreadyClaimedLocalIDs); + if (alreadyClaimedLocalIDs.isNotEmpty) { + await _db.removeQueuedLocalFiles(alreadyClaimedLocalIDs); + } } if (localIDsToSync.isEmpty) { From 98868dd76f302a590e4accc1a32f7ef99f38e1cb Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 24 Jul 2025 11:40:26 +0530 Subject: [PATCH 15/29] [web][auth] Lint fix --- web/apps/auth/src/pages/auth.tsx | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/web/apps/auth/src/pages/auth.tsx b/web/apps/auth/src/pages/auth.tsx index 5c9809c9c6..f74f0e7c8f 100644 --- a/web/apps/auth/src/pages/auth.tsx +++ b/web/apps/auth/src/pages/auth.tsx @@ -404,10 +404,7 @@ const Footer: React.FC = () => { return ( {t("auth_download_mobile_app")} - + From daaf73664a250945cdd844d30c40e7cb72043f0f Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 24 Jul 2025 13:14:09 +0530 Subject: [PATCH 16/29] [server][db] Tweak autovacuum threshold for trash table --- .../102_trash_vaccume_and_analyze_threshold.down.sql | 6 ++++++ .../102_trash_vaccume_and_analyze_threshold.up.sql | 6 ++++++ 2 files changed, 12 insertions(+) create mode 100644 server/migrations/102_trash_vaccume_and_analyze_threshold.down.sql create mode 100644 server/migrations/102_trash_vaccume_and_analyze_threshold.up.sql diff --git a/server/migrations/102_trash_vaccume_and_analyze_threshold.down.sql b/server/migrations/102_trash_vaccume_and_analyze_threshold.down.sql new file mode 100644 index 0000000000..3d2dc0b0bf --- /dev/null +++ b/server/migrations/102_trash_vaccume_and_analyze_threshold.down.sql @@ -0,0 +1,6 @@ +ALTER TABLE trash RESET ( + autovacuum_analyze_scale_factor, + autovacuum_vacuum_scale_factor, + autovacuum_analyze_threshold, + autovacuum_vacuum_threshold + ); \ No newline at end of file diff --git a/server/migrations/102_trash_vaccume_and_analyze_threshold.up.sql b/server/migrations/102_trash_vaccume_and_analyze_threshold.up.sql new file mode 100644 index 0000000000..5d61312da7 --- /dev/null +++ b/server/migrations/102_trash_vaccume_and_analyze_threshold.up.sql @@ -0,0 +1,6 @@ +ALTER TABLE trash SET ( + autovacuum_analyze_scale_factor = 0.01, -- Trigger ANALYZE after 1% of rows change + autovacuum_vacuum_scale_factor = 0.02, -- Trigger VACUUM after 2% of rows change + autovacuum_analyze_threshold = 1000, + autovacuum_vacuum_threshold = 1000 +); From 0b8f26d0bbd9585baefab22f3873940d13b6286a Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Thu, 24 Jul 2025 14:12:46 +0530 Subject: [PATCH 17/29] Fix dare parsing file --- .../lib/services/date_parse_service.dart | 579 +++++++++--------- .../photos/lib/services/search_service.dart | 118 ++-- 2 files changed, 342 insertions(+), 355 deletions(-) diff --git a/mobile/apps/photos/lib/services/date_parse_service.dart b/mobile/apps/photos/lib/services/date_parse_service.dart index f70bafe2ea..ef136d25ed 100644 --- a/mobile/apps/photos/lib/services/date_parse_service.dart +++ b/mobile/apps/photos/lib/services/date_parse_service.dart @@ -1,12 +1,54 @@ -import "package:photos/data/months.dart"; -import 'package:tuple/tuple.dart'; +import 'dart:collection'; + +class PartialDate { + final int? day; + final int? month; + final int? year; + + const PartialDate({this.day, this.month, this.year}); + + static const empty = PartialDate(); + + bool get isEmpty => day == null && month == null && year == null; + + @override + bool operator ==(Object other) => + identical(this, other) || + other is PartialDate && + runtimeType == other.runtimeType && + day == other.day && + month == other.month && + year == other.year; + + @override + int get hashCode => day.hashCode ^ month.hashCode ^ year.hashCode; + + @override + String toString() { + return 'PartialDate(day: $day, month: $month, year: $year)'; + } +} class DateParseService { - static final DateParseService instance = - DateParseService._privateConstructor(); - DateParseService._privateConstructor(); + static final DateParseService instance = DateParseService._private(); + DateParseService._private(); - static const Map _monthMap = { + static const int _MIN_YEAR = 1900; + static const int _MAX_YEAR = 2100; + static const int _TWO_DIGIT_YEAR_PIVOT = 50; + + static final _ordinalRegex = RegExp(r'\b(\d{1,2})(st|nd|rd|th)\b'); + static final _normalizeRegex = RegExp(r'\bof\b|[,\.]+|\s+'); + static final _isoFormatRegex = + RegExp(r'^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$'); + static final _standardFormatRegex = + RegExp(r'^(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})$'); + static final _dotFormatRegex = RegExp(r'^(\d{1,2})\.(\d{1,2})\.(\d{2,4})$'); + static final _compactFormatRegex = RegExp(r'^(\d{8})$'); + static final _yearOnlyRegex = RegExp(r'^\s*(\d{4})\s*$'); + static final _shortFormatRegex = RegExp(r'^(\d{1,2})[\/-](\d{1,2})$'); + + static final Map _monthMap = UnmodifiableMapView({ "january": 1, "february": 2, "march": 3, @@ -41,7 +83,7 @@ class DateParseService { "octo": 10, "nove": 11, "dece": 12, - }; + }); static const Map monthNumberToName = { 1: "January", @@ -58,323 +100,266 @@ class DateParseService { 12: "December", }; - String normalizeDateString(String input) { + PartialDate parse(String input) { + if (input.trim().isEmpty) return PartialDate.empty; + + final lowerInput = input.toLowerCase(); + + var result = _parseRelativeDate(lowerInput); + if (!result.isEmpty) return result; + + result = _parseStructuredFormats(lowerInput); + if (!result.isEmpty) return result; + + final normalized = _normalizeDateString(lowerInput); + result = _parseTokenizedDate(normalized); + + return result; + } + + String getMonthName(int month) { + return monthNumberToName[month] ?? 'Unknown'; + } + + String _normalizeDateString(String input) { return input - .toLowerCase() - .replaceAllMapped( - RegExp(r'\b(\d{1,2})(st|nd|rd|th)\b'), - (match) => match.group(1)!, - ) - .replaceAll(RegExp(r'\bof\b'), '') - .replaceAll(RegExp(r'[,\.]+'), '') - .replaceAll(RegExp(r'\s+'), ' ') + .replaceAllMapped(_ordinalRegex, (match) => match.group(1)!) + .replaceAll(_normalizeRegex, ' ') .trim(); } - Tuple3 parseStructuredFormats(String input) { - final normalized = input.replaceAll(RegExp(r'\s'), ''); - - // ISO format: YYYY-MM-DD or YYYY/MM/DD - final isoMatch = - RegExp(r'^(\d{4})[\/-](\d{1,2})[\/-](\d{1,2})$').firstMatch(normalized); - if (isoMatch != null) { - return Tuple3( - int.tryParse(isoMatch.group(3)!), // day - int.tryParse(isoMatch.group(2)!), // month - int.tryParse(isoMatch.group(1)!), // year - ); - } - - // Standard formats: MM/DD/YYYY, DD/MM/YYYY, MM-DD-YYYY, DD-MM-YYYY - final standardMatch = - RegExp(r'^(\d{1,2})[\/-](\d{1,2})[\/-](\d{4})$').firstMatch(normalized); - if (standardMatch != null) { - final first = int.tryParse(standardMatch.group(1)!); - final second = int.tryParse(standardMatch.group(2)!); - final year = int.tryParse(standardMatch.group(3)!); - - if (first == null || second == null || year == null) { - return const Tuple3(null, null, null); - } - if (first < 1 || first > 31 || second < 1 || second > 31) { - return const Tuple3(null, null, null); - } - - // Heuristic: if first number > 12, assume DD/MM format - if (first > 12) { - if (second > 12) return const Tuple3(null, null, null); - return Tuple3(first, second, year); - } else { - if (second > 12) return const Tuple3(null, null, null); - return Tuple3(second, first, year); - } - } - - // Standard formats with 2-digit years: MM/DD/YY, DD/MM/YY, MM-DD-YY, DD-MM-YY - final standardMatchTwoDigitYear = - RegExp(r'^(\d{1,2})[\/-](\d{1,2})[\/-](\d{2})$').firstMatch(normalized); - if (standardMatchTwoDigitYear != null) { - final first = int.tryParse(standardMatchTwoDigitYear.group(1)!); - final second = int.tryParse(standardMatchTwoDigitYear.group(2)!); - final yearTwoDigit = int.tryParse(standardMatchTwoDigitYear.group(3)!); - - if (first == null || second == null || yearTwoDigit == null) { - return const Tuple3(null, null, null); - } - if (first < 1 || first > 31 || second < 1 || second > 31) { - return const Tuple3(null, null, null); - } - - final year = - yearTwoDigit < 50 ? 2000 + yearTwoDigit : 1900 + yearTwoDigit; - - if (first > 12) { - if (second > 12) return const Tuple3(null, null, null); - return Tuple3(first, second, year); - } else { - if (second > 12) return const Tuple3(null, null, null); - return Tuple3(second, first, year); - } - } - - // Dot format: DD.MM.YYYY - final dotMatch = - RegExp(r'^(\d{1,2})\.(\d{1,2})\.(\d{4})$').firstMatch(normalized); - if (dotMatch != null) { - final day = int.tryParse(dotMatch.group(1)!); - final month = int.tryParse(dotMatch.group(2)!); - final year = int.tryParse(dotMatch.group(3)!); - - if (day == null || month == null || year == null) { - return const Tuple3(null, null, null); - } - if (day < 1 || day > 31 || month < 1 || month > 12) { - return const Tuple3(null, null, null); - } - - return Tuple3(day, month, year); - } - - // Dot format with 2-digit year: DD.MM.YY - final dotMatchTwoDigitYear = - RegExp(r'^(\d{1,2})\.(\d{1,2})\.(\d{2})$').firstMatch(normalized); - if (dotMatchTwoDigitYear != null) { - final day = int.tryParse(dotMatchTwoDigitYear.group(1)!); - final month = int.tryParse(dotMatchTwoDigitYear.group(2)!); - final yearTwoDigit = int.tryParse(dotMatchTwoDigitYear.group(3)!); - - if (day == null || month == null || yearTwoDigit == null) { - return const Tuple3(null, null, null); - } - if (day < 1 || day > 31 || month < 1 || month > 12) { - return const Tuple3(null, null, null); - } - - final year = - yearTwoDigit < 50 ? 2000 + yearTwoDigit : 1900 + yearTwoDigit; - - return Tuple3(day, month, year); - } - - // Compact format: YYYYMMDD - if (normalized.length == 8 && RegExp(r'^\d{8}$').hasMatch(normalized)) { - final year = int.tryParse(normalized.substring(0, 4)); - final month = int.tryParse(normalized.substring(4, 6)); - final day = int.tryParse(normalized.substring(6, 8)); - if (year != null && - year > 1900 && - month != null && - month <= 12 && - day != null && - day <= 31) { - return Tuple3(day, month, year); - } - } - - // Short format: MM/DD or DD/MM - final shortMatch = - RegExp(r'^(\d{1,2})[\/-](\d{1,2})$').firstMatch(normalized); - if (shortMatch != null) { - final first = int.tryParse(shortMatch.group(1)!); - final second = int.tryParse(shortMatch.group(2)!); - - if (first == null || second == null) { - return const Tuple3(null, null, null); - } - if (first < 1 || first > 31 || second < 1 || second > 31) { - return const Tuple3(null, null, null); - } - - if (first > 12) { - if (second > 12) return const Tuple3(null, null, null); - return Tuple3(first, second, null); - } else { - if (second > 12) return const Tuple3(null, null, null); - return Tuple3(second, first, null); - } - } - - return const Tuple3(null, null, null); + int _convertTwoDigitYear(int year) { + return year < _TWO_DIGIT_YEAR_PIVOT ? 2000 + year : 1900 + year; } - Tuple3 _parseDateParts(String input) { - if (input.trim().isEmpty) return const Tuple3(null, null, null); + PartialDate _parseRelativeDate(String lowerInput) { + final bool hasToday = lowerInput.contains('today'); + final bool hasTomorrow = lowerInput.contains('tomorrow'); + final bool hasYesterday = lowerInput.contains('yesterday'); - final lowerInput = input.toLowerCase(); - final today = DateTime.now(); + final int count = + (hasToday ? 1 : 0) + (hasTomorrow ? 1 : 0) + (hasYesterday ? 1 : 0); - if (lowerInput.contains('today')) { - return Tuple3( - today.day, - MonthData(monthNumberToName[today.month]!, today.month), - today.year, - ); - } else if (lowerInput.contains('tomorrow')) { - final tomorrow = today.add(const Duration(days: 1)); - return Tuple3( - tomorrow.day, - MonthData(monthNumberToName[tomorrow.month]!, tomorrow.month), - tomorrow.year, - ); - } else if (lowerInput.contains('yesterday')) { - final yesterday = today.subtract(const Duration(days: 1)); - return Tuple3( - yesterday.day, - MonthData(monthNumberToName[yesterday.month]!, yesterday.month), - yesterday.year, + if (count > 1) { + return PartialDate.empty; + } + + final now = DateTime.now(); + if (hasToday) { + return PartialDate(day: now.day, month: now.month, year: now.year); + } + if (hasTomorrow) { + final tomorrow = now.add(const Duration(days: 1)); + return PartialDate( + day: tomorrow.day, + month: tomorrow.month, + year: tomorrow.year, ); } + if (hasYesterday) { + final yesterday = now.subtract(const Duration(days: 1)); + return PartialDate( + day: yesterday.day, + month: yesterday.month, + year: yesterday.year, + ); + } + return PartialDate.empty; + } - // Check for year-only queries like "2025" - final yearOnlyMatch = RegExp(r'^\s*(\d{4})\s*$').firstMatch(input.trim()); - if (yearOnlyMatch != null) { - final year = int.tryParse(yearOnlyMatch.group(1)!); - if (year != null && year >= 1900 && year <= 2100) { - return Tuple3(null, null, year); + PartialDate _parseStructuredFormats(String input) { + final cleanInput = input.replaceAll(' ', ''); + + Match? match = _isoFormatRegex.firstMatch(cleanInput); + if (match != null) { + final yearVal = int.tryParse(match.group(1)!); + final monthVal = int.tryParse(match.group(2)!); + final dayVal = int.tryParse(match.group(3)!); + if (yearVal != null && + yearVal >= _MIN_YEAR && + yearVal <= _MAX_YEAR && + monthVal != null && + monthVal >= 1 && + monthVal <= 12 && + dayVal != null && + dayVal >= 1 && + dayVal <= 31) { + return PartialDate(day: dayVal, month: monthVal, year: yearVal); } + return PartialDate.empty; } - // First try structured formats (slash, dash, dot patterns) - final structuredResult = parseStructuredFormats(input); - if (structuredResult.item1 != null || - structuredResult.item2 != null || - structuredResult.item3 != null) { - final int? day = structuredResult.item1; - final int? monthNum = structuredResult.item2; - final int? year = structuredResult.item3; - MonthData? monthData; - if (monthNum != null && monthNumberToName.containsKey(monthNum)) { - monthData = MonthData(monthNumberToName[monthNum]!, monthNum); - } - return Tuple3(day, monthData, year); - } + match = _standardFormatRegex.firstMatch(cleanInput); + if (match != null) { + final p1 = int.parse(match.group(1)!); + final p2 = int.parse(match.group(2)!); + final yearRaw = int.parse(match.group(3)!); + final year = yearRaw > 99 ? yearRaw : _convertTwoDigitYear(yearRaw); - final normalized = normalizeDateString(input); - final tokens = normalized.split(RegExp(r'\s+')); + if (year < _MIN_YEAR || year > _MAX_YEAR) return PartialDate.empty; - int? day, monthNum, year; - MonthData? monthData; - - // Handle patterns like "25 02" (day month) or "Feb 2025" (month year) - if (tokens.length == 2) { - final first = tokens[0]; - final second = tokens[1]; - - // Check if first token is a month name and second is a year - if (_monthMap.containsKey(first)) { - final yearValue = int.tryParse(second); - if (yearValue != null && yearValue >= 1900 && yearValue <= 2100) { - monthNum = _monthMap[first]; - year = yearValue; + if (p1 > 12) { + if (p1 >= 1 && p1 <= 31 && p2 >= 1 && p2 <= 12) { + return PartialDate(day: p1, month: p2, year: year); + } + } else if (p2 > 12) { + if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { + return PartialDate(day: p2, month: p1, year: year); + } + } else { + if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { + return PartialDate(day: p2, month: p1, year: year); } } - // Check if both are numbers - could be day+month or month+day - else { - final firstNum = int.tryParse(first); - final secondNum = int.tryParse(second); + return PartialDate.empty; + } - if (firstNum != null && secondNum != null) { - if (secondNum >= 1900 && secondNum <= 2100) { - if (firstNum >= 1 && firstNum <= 12) { - monthNum = firstNum; - year = secondNum; - } - } else if (firstNum >= 1 && - firstNum <= 31 && - secondNum >= 1 && - secondNum <= 12) { - day = firstNum; - monthNum = secondNum; - } else if (firstNum >= 1 && - firstNum <= 12 && - secondNum >= 1 && - secondNum <= 31) { - monthNum = firstNum; - day = secondNum; - } + match = _shortFormatRegex.firstMatch(cleanInput); + if (match != null) { + final p1 = int.parse(match.group(1)!); + final p2 = int.parse(match.group(2)!); + + if (p1 > 12) { + if (p1 >= 1 && p1 <= 31 && p2 >= 1 && p2 <= 12) { + return PartialDate(day: p1, month: p2); } + } else if (p2 > 12) { + if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { + return PartialDate(day: p2, month: p1); + } + } else { + if (p1 >= 1 && p1 <= 12 && p2 >= 1 && p2 <= 31) { + return PartialDate(day: p2, month: p1); + } + } + return PartialDate.empty; + } + + match = _dotFormatRegex.firstMatch(cleanInput); + if (match != null) { + final yearRaw = int.parse(match.group(3)!); + final year = yearRaw > 99 ? yearRaw : _convertTwoDigitYear(yearRaw); + final dayVal = int.tryParse(match.group(1)!); + final monthVal = int.tryParse(match.group(2)!); + + if (year >= _MIN_YEAR && + year <= _MAX_YEAR && + dayVal != null && + dayVal >= 1 && + dayVal <= 31 && + monthVal != null && + monthVal >= 1 && + monthVal <= 12) { + return PartialDate(day: dayVal, month: monthVal, year: year); + } + return PartialDate.empty; + } + + match = _compactFormatRegex.firstMatch(cleanInput); + if (match != null) { + final yearVal = int.tryParse(cleanInput.substring(0, 4)); + final monthVal = int.tryParse(cleanInput.substring(4, 6)); + final dayVal = int.tryParse(cleanInput.substring(6, 8)); + + if (yearVal != null && + yearVal >= _MIN_YEAR && + yearVal <= _MAX_YEAR && + monthVal != null && + monthVal >= 1 && + monthVal <= 12 && + dayVal != null && + dayVal >= 1 && + dayVal <= 31) { + return PartialDate(day: dayVal, month: monthVal, year: yearVal); + } + return PartialDate.empty; + } + + return PartialDate.empty; + } + + PartialDate _parseTokenizedDate(String normalized) { + final tokens = normalized.split(' '); + int? day, month, year; + + if (tokens.length == 1) { + final token = tokens[0]; + final match = _yearOnlyRegex.firstMatch(token); + if (match != null) { + final parsedYear = int.tryParse(match.group(1)!); + if (parsedYear != null && + parsedYear >= _MIN_YEAR && + parsedYear <= _MAX_YEAR) { + return PartialDate(year: parsedYear); + } + } + if (_monthMap.containsKey(token)) { + return PartialDate(month: _monthMap[token]!); + } + final singleValue = int.tryParse(token); + if (singleValue != null && singleValue >= 1 && singleValue <= 31) { + return PartialDate(day: singleValue); + } + return PartialDate.empty; + } + + for (final token in tokens) { + if (_monthMap.containsKey(token) && month == null) { + month = _monthMap[token]; + continue; + } + + final value = int.tryParse(token); + if (value == null) { + continue; + } + + if (value >= _MIN_YEAR && value <= _MAX_YEAR && year == null) { + year = value; + } else if (value >= 1 && value <= 31 && day == null) { + day = value; + } else if (value >= 0 && value <= 99 && year == null) { + final convertedYear = _convertTwoDigitYear(value); + if (convertedYear >= _MIN_YEAR && convertedYear <= _MAX_YEAR) { + year = convertedYear; + } + } else if (value >= 1 && value <= 12 && month == null) { + month = value; } } - if (day == null && monthNum == null && year == null) { - for (var token in tokens) { - if (_monthMap.containsKey(token)) { - monthNum = _monthMap[token]; - } else if (RegExp(r'^\d+$').hasMatch(token)) { - final value = int.parse(token); - if (value >= 1900 && value <= 2100) { - year = value; - } else if (value >= 1 && value <= 31) { - if (day == null) { - day = value; - } else if (monthNum == null && value <= 12) { - monthNum = value; - } - } else if (value >= 32 && value <= 99) { - year = value < 50 ? 2000 + value : 1900 + value; - } - } - } - } - - if (monthNum != null && monthNumberToName.containsKey(monthNum)) { - monthData = MonthData(monthNumberToName[monthNum]!, monthNum); - } - - if (monthNum != null && (monthNum < 1 || monthNum > 12)) { - monthNum = null; - monthData = null; - } if (day != null && (day < 1 || day > 31)) { day = null; } - - return Tuple3(day, monthData, year); - } - - Tuple3 parseDate(String input) { - return _parseDateParts(input); - } - - List> parseDateVariations(String input) { - final List> variations = []; - final primaryResult = _parseDateParts(input); - - variations.add(primaryResult); - return variations; - } - - bool isYearQuery(String input) { - final yearOnlyMatch = RegExp(r'^\s*(\d{4})\s*$').firstMatch(input.trim()); - if (yearOnlyMatch != null) { - final year = int.tryParse(yearOnlyMatch.group(1)!); - return year != null && year >= 1900 && year <= 2100; + if (month != null && (month < 1 || month > 12)) { + month = null; } - return false; - } - bool isGenericDateQuery(String input) { - final result = _parseDateParts(input); - return result.item1 != null && result.item2 != null && result.item3 == null; + final bool inputHadMonthWord = tokens.any((t) => _monthMap.containsKey(t)); + final bool inputHadYearWord = tokens.any((t) { + final v = int.tryParse(t); + return v != null && v >= 1000 && v <= 9999; + }); + + if (day != null && month == null && year == null && tokens.length > 1) { + if (normalized.contains('of') && + !inputHadMonthWord && + !inputHadYearWord) { + return PartialDate.empty; + } + if (!inputHadMonthWord && !inputHadYearWord && tokens.length > 1) {} + } + + if (day == null && month == null && year == null) { + return PartialDate.empty; + } + + if (day != null && month == null && year == null && tokens.length > 1) { + if (!inputHadMonthWord && !inputHadYearWord) { + return PartialDate.empty; + } + } + + return PartialDate(day: day, month: month, year: year); } } diff --git a/mobile/apps/photos/lib/services/search_service.dart b/mobile/apps/photos/lib/services/search_service.dart index f09007c6ee..f1dd0d64f4 100644 --- a/mobile/apps/photos/lib/services/search_service.dart +++ b/mobile/apps/photos/lib/services/search_service.dart @@ -1173,70 +1173,72 @@ class SearchService { ) async { final List searchResults = []; - final dateVariations = DateParseService.instance.parseDateVariations(query); + final parsedDate = DateParseService.instance.parse(query); - for (final dateVar in dateVariations) { - // Handle month-year queries - if (dateVar.item1 == null && - dateVar.item2 != null && - dateVar.item3 != null) { - final month = dateVar.item2!.monthNumber; - final year = dateVar.item3!; - final monthYearFiles = - await FilesDB.instance.getFilesCreatedWithinDurations( - [_getDurationForMonthInYear(month, year)], - ignoreCollections(), - order: 'DESC', - ); - if (monthYearFiles.isNotEmpty) { - final name = '${dateVar.item2!.name} $year'; - searchResults.add( - GenericSearchResult( - ResultType.month, - name, - monthYearFiles, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.month, - matchedUploadedIDs: filesToUploadedFileIDs(monthYearFiles), - filterIcon: Icons.calendar_month_outlined, - ), + if (parsedDate.isEmpty) { + return searchResults; + } + // Handle month-year queries + if (parsedDate.day == null && + parsedDate.month != null && + parsedDate.year != null) { + final month = parsedDate.month!; + final year = parsedDate.year!; + final monthYearFiles = + await FilesDB.instance.getFilesCreatedWithinDurations( + [_getDurationForMonthInYear(month, year)], + ignoreCollections(), + order: 'DESC', + ); + if (monthYearFiles.isNotEmpty) { + final monthName = DateParseService.instance.getMonthName(month); + final name = '$monthName $year'; + searchResults.add( + GenericSearchResult( + ResultType.month, + name, + monthYearFiles, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.month, + matchedUploadedIDs: filesToUploadedFileIDs(monthYearFiles), + filterIcon: Icons.calendar_month_outlined, ), - ); - } + ), + ); } - // Handle day-month queries (with or without year) - else if (dateVar.item1 != null && dateVar.item2 != null) { - final int day = dateVar.item1!; - final int month = dateVar.item2!.monthNumber; - final int? year = dateVar.item3; // nullable for generic dates + } + // Handle day-month queries (with or without year) + else if (parsedDate.day != null && parsedDate.month != null) { + final int day = parsedDate.day!; + final int month = parsedDate.month!; + final int? year = parsedDate.year; // nullable for generic dates - final matchedFiles = - await FilesDB.instance.getFilesCreatedWithinDurations( - _getDurationsForCalendarDateInEveryYear(day, month, year: year), - ignoreCollections(), - order: 'DESC', - ); + final matchedFiles = + await FilesDB.instance.getFilesCreatedWithinDurations( + _getDurationsForCalendarDateInEveryYear(day, month, year: year), + ignoreCollections(), + order: 'DESC', + ); - if (matchedFiles.isNotEmpty) { - final name = - '$day ${dateVar.item2!.name}${year != null ? ' $year' : ''}'; - searchResults.add( - GenericSearchResult( - ResultType.event, - name, - matchedFiles, - hierarchicalSearchFilter: TopLevelGenericFilter( - filterName: name, - occurrence: kMostRelevantFilter, - filterResultType: ResultType.event, - matchedUploadedIDs: filesToUploadedFileIDs(matchedFiles), - filterIcon: Icons.event_outlined, - ), + if (matchedFiles.isNotEmpty) { + final monthName = DateParseService.instance.getMonthName(month); + final name = '$day $monthName${year != null ? ' $year' : ''}'; + searchResults.add( + GenericSearchResult( + ResultType.event, + name, + matchedFiles, + hierarchicalSearchFilter: TopLevelGenericFilter( + filterName: name, + occurrence: kMostRelevantFilter, + filterResultType: ResultType.event, + matchedUploadedIDs: filesToUploadedFileIDs(matchedFiles), + filterIcon: Icons.event_outlined, ), - ); - } + ), + ); } } return searchResults; From 241dcd64bfc28269f037a598f67afcf16e731f69 Mon Sep 17 00:00:00 2001 From: AmanRajSinghMourya Date: Thu, 24 Jul 2025 14:13:04 +0530 Subject: [PATCH 18/29] Add comprehensive tests for date parsing functionality --- .../test/utils/date_query_parsing_test.dart | 346 ++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100644 mobile/apps/photos/test/utils/date_query_parsing_test.dart diff --git a/mobile/apps/photos/test/utils/date_query_parsing_test.dart b/mobile/apps/photos/test/utils/date_query_parsing_test.dart new file mode 100644 index 0000000000..402b890489 --- /dev/null +++ b/mobile/apps/photos/test/utils/date_query_parsing_test.dart @@ -0,0 +1,346 @@ +import 'package:photos/services/date_parse_service.dart'; +import 'package:test/test.dart'; + +void main() { + // Get an instance of the service + final DateParseService dateParseService = DateParseService.instance; + + // --- Natural Language Date Parsing --- + group('Natural Language Date Parsing', () { + // Relative dates: today, tomorrow, yesterday + test('should parse "today" correctly', () { + final DateTime now = DateTime.now(); + final PartialDate expectedDate = + PartialDate(day: now.day, month: now.month, year: now.year); + final PartialDate parsedDate = dateParseService.parse('today'); + + expect( + parsedDate.day, + expectedDate.day, + reason: 'Day mismatch for today', + ); + expect( + parsedDate.month, + expectedDate.month, + reason: 'Month mismatch for today', + ); + expect( + parsedDate.year, + expectedDate.year, + reason: 'Year mismatch for today', + ); + }); + + test('should parse "tomorrow" correctly', () { + final DateTime tomorrow = DateTime.now().add(const Duration(days: 1)); + final PartialDate expectedDate = PartialDate( + day: tomorrow.day, + month: tomorrow.month, + year: tomorrow.year, + ); + final PartialDate parsedDate = dateParseService.parse('tomorrow'); + + expect( + parsedDate.day, + expectedDate.day, + reason: 'Day mismatch for tomorrow', + ); + expect( + parsedDate.month, + expectedDate.month, + reason: 'Month mismatch for tomorrow', + ); + expect( + parsedDate.year, + expectedDate.year, + reason: 'Year mismatch for tomorrow', + ); + }); + + test('should parse "yesterday" correctly', () { + final DateTime yesterday = + DateTime.now().subtract(const Duration(days: 1)); + final PartialDate expectedDate = PartialDate( + day: yesterday.day, + month: yesterday.month, + year: yesterday.year, + ); + final PartialDate parsedDate = dateParseService.parse('yesterday'); + + expect( + parsedDate.day, + expectedDate.day, + reason: 'Day mismatch for yesterday', + ); + expect( + parsedDate.month, + expectedDate.month, + reason: 'Month mismatch for yesterday', + ); + expect( + parsedDate.year, + expectedDate.year, + reason: 'Year mismatch for yesterday', + ); + }); + + // Month names: Full (February), abbreviated (Feb), and partial (Febr) + test('should parse full month name "February 2025"', () { + final PartialDate parsedDate = dateParseService.parse('February 2025'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse abbreviated month name "Feb 2025"', () { + final PartialDate parsedDate = dateParseService.parse('Feb 2025'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse partial month name "Febr 2025"', () { + final PartialDate parsedDate = dateParseService.parse('Febr 2025'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + // Ordinal numbers: 25th, 22nd, 3rd, 1st + test('should parse ordinal number "25th Jan 2024"', () { + final PartialDate parsedDate = dateParseService.parse('25th Jan 2024'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 1); + expect(parsedDate.year, 2024); + }); + + test('should parse ordinal number "22nd Feb"', () { + final PartialDate parsedDate = dateParseService.parse('22nd Feb'); + expect(parsedDate.day, 22); + expect(parsedDate.month, 2); + expect(parsedDate.year, isNull); + }); + + test('should parse ordinal number "3rd March"', () { + final PartialDate parsedDate = dateParseService.parse('3rd March'); + expect(parsedDate.day, 3); + expect(parsedDate.month, 3); + expect(parsedDate.year, isNull); + }); + + // Flexible combinations + test('should parse "25th Feb" (generic date)', () { + final PartialDate parsedDate = dateParseService.parse('25th Feb'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, isNull); + }); + + test('should parse "February 2025" (month-year query)', () { + final PartialDate parsedDate = dateParseService.parse('February 2025'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse "25th of February 2025"', () { + final PartialDate parsedDate = + dateParseService.parse('25th of February 2025'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + }); + + // --- Structured Date Format Support --- + group('Structured Date Format Support', () { + // ISO format: 2025-02-25, 2025/02/25 + test('should parse ISO format "2025-02-25"', () { + final PartialDate parsedDate = dateParseService.parse('2025-02-25'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse ISO format "2025/02/25"', () { + final PartialDate parsedDate = dateParseService.parse('2025/02/25'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + // Standard formats: 02/25/2025, 25/02/2025 (with MM/DD vs DD/MM detection) + test('should parse standard MM/DD/YYYY format "02/25/2025"', () { + // Your parser assumes MM/DD if ambiguous (e.g., both parts <= 12) + // but for 02/25/2025, 25 > 12, so it correctly interprets 02 as month and 25 as day. + final PartialDate parsedDate = dateParseService.parse('02/25/2025'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse standard DD/MM/YYYY format "25/02/2025"', () { + // Your parser handles DD/MM explicitly when day part > 12 + final PartialDate parsedDate = dateParseService.parse('25/02/2025'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse ambiguous "01/02/2024" as MM/DD/YYYY (Jan 2)', () { + // Test your specific heuristic for ambiguous cases + final PartialDate parsedDate = dateParseService.parse('01/02/2024'); + expect(parsedDate.day, 2); + expect(parsedDate.month, 1); + expect(parsedDate.year, 2024); + }); + + // Dot notation: 25.02.2025, 25.02.25 + test('should parse dot notation "25.02.2025"', () { + final PartialDate parsedDate = dateParseService.parse('25.02.2025'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse dot notation with two-digit year "25.02.25"', () { + // Assumes century detection (e.g., 25 -> 2025) + final PartialDate parsedDate = dateParseService.parse('25.02.25'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + // Compact format: 20250225 + test('should parse compact format "20250225"', () { + final PartialDate parsedDate = dateParseService.parse('20250225'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + // Short formats: 02/25, 25/02 (your parser doesn't explicitly handle short yearless formats) + // Based on your _standardFormatRegex: RegExp(r'^(\d{1,2})[\/-](\d{1,2})[\/-](\d{2,4})$'); + // and _parseTokenizedDate, "02/25" would be processed by _parseTokenizedDate. + // Let's test how your current parser handles these. + test( + 'should parse short MM/DD format "02/25" (no year, handled by tokenized)', + () { + final PartialDate parsedDate = dateParseService.parse('02/25'); + expect(parsedDate.day, 25); // value 25 is assigned to day first + expect(parsedDate.month, 2); // value 02 is assigned to month + expect(parsedDate.year, isNull); + }); + + test( + 'should parse short DD/MM format "25/02" (no year, handled by tokenized)', + () { + // This will be parsed by _parseTokenizedDate + final PartialDate parsedDate = dateParseService.parse('25/02'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, isNull); + }); + + // Two-digit years: 25/02/25 (with century detection) + test('should parse two-digit year "25/02/25"', () { + final PartialDate parsedDate = dateParseService.parse('25/02/25'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); // Based on _convertTwoDigitYear pivot + }); + + test('should parse two-digit year "01/01/01" as 2001', () { + final PartialDate parsedDate = dateParseService.parse('01/01/01'); + expect(parsedDate.day, 1); + expect(parsedDate.month, 1); + expect(parsedDate.year, 2001); // 01 < _TWO_DIGIT_YEAR_PIVOT + }); + + test('should parse two-digit year "01/01/99" as 1999', () { + final PartialDate parsedDate = dateParseService.parse('01/01/99'); + expect(parsedDate.day, 1); + expect(parsedDate.month, 1); + expect(parsedDate.year, 1999); // 99 > _TWO_DIGIT_YEAR_PIVOT + }); + }); + + // --- Smart Query Types --- + group('Smart Query Types', () { + test('should parse year-only query "2025"', () { + final PartialDate parsedDate = dateParseService.parse('2025'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, isNull); + expect(parsedDate.year, 2025); + }); + + test('should parse month-year query "February 2025"', () { + final PartialDate parsedDate = dateParseService.parse('February 2025'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + + test('should parse generic date query "25th Feb" (year is null)', () { + final PartialDate parsedDate = dateParseService.parse('25th Feb'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, isNull); + }); + + test('should parse specific date query "25/02/2025"', () { + final PartialDate parsedDate = dateParseService.parse('25/02/2025'); + expect(parsedDate.day, 25); + expect(parsedDate.month, 2); + expect(parsedDate.year, 2025); + }); + }); + + // --- Invalid Date Queries --- + group('Invalid Date Queries', () { + test('should parse "February 30000" as month-only (invalid year ignored)', + () { + final PartialDate parsedDate = dateParseService.parse('February 30000'); + expect(parsedDate.day, isNull); + expect(parsedDate.month, 2); + expect( + parsedDate.year, + isNull, + reason: 'Year 30000 is out of range and should be ignored', + ); + }); + + // Specific case to test if invalid day/month values are set to null + test('should return null for invalid day/month in tokenized parsing', () { + final PartialDate parsedDate = dateParseService.parse('32 Jan 2024'); + expect(parsedDate.day, isNull, reason: 'Day should be null for 32'); + expect(parsedDate.month, 1); + expect( + parsedDate.year, + 2024, + ); + + // "Jan 13 2024" - This is a valid date (Jan 13, 2024), should parse completely. + final PartialDate parsedDate2 = dateParseService.parse('Jan 13 2024'); + expect(parsedDate2.day, 13); + expect(parsedDate2.month, 1); + expect(parsedDate2.year, 2024); + + // "Feb 0 2024" - Day 0 should be null, but month and year are valid. + final PartialDate parsedDate3 = dateParseService.parse('Feb 0 2024'); + expect(parsedDate3.day, isNull, reason: 'Day should be null for 0'); + expect(parsedDate3.month, 2); + expect(parsedDate3.year, 2024); + }); + + test('should handle invalid day/month in tokenized parsing gracefully', () { + final PartialDate parsedDate = dateParseService.parse('32 Jan 2024'); + expect(parsedDate.day, isNull, reason: 'Day should be null for 32'); + expect(parsedDate.month, 1); + expect( + parsedDate.year, + 2024, + ); + }); + }); +} From f951880ed1b17a2935266617a01e07241ae4c3cb Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 24 Jul 2025 15:16:24 +0530 Subject: [PATCH 19/29] [server] Use IS FALSE to match partial index --- server/pkg/repo/trash.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/pkg/repo/trash.go b/server/pkg/repo/trash.go index 1c6ab6b45f..f1ab3c2289 100644 --- a/server/pkg/repo/trash.go +++ b/server/pkg/repo/trash.go @@ -338,7 +338,7 @@ func (t *TrashRepository) GetTimeStampForLatestNonDeletedEntry(userID int64) (*i // GetUserIDToFileIDsMapForDeletion returns map of userID to fileIds, where the file ids which should be deleted by now func (t *TrashRepository) GetUserIDToFileIDsMapForDeletion() (map[int64][]int64, error) { rows, err := t.DB.Query(`SELECT user_id, file_id FROM trash - WHERE delete_by <= $1 AND is_deleted = FALSE AND is_restored = FALSE limit $2`, + WHERE delete_by <= $1 AND is_deleted IS FALSE AND is_restored IS FALSE limit $2`, time.Microseconds(), TrashDiffLimit) if err != nil { return nil, stacktrace.Propagate(err, "") From 0906fddfc638673568cfcbc58ae7dfcd73415743 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 24 Jul 2025 16:24:54 +0530 Subject: [PATCH 20/29] [mob][photos] Fix query to remove dup enteries --- mobile/apps/photos/lib/db/files_db.dart | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/mobile/apps/photos/lib/db/files_db.dart b/mobile/apps/photos/lib/db/files_db.dart index 3150dbc3b6..657148a61a 100644 --- a/mobile/apps/photos/lib/db/files_db.dart +++ b/mobile/apps/photos/lib/db/files_db.dart @@ -982,15 +982,23 @@ class FilesDB with SqlDbBase { // remove references for local files which are either already uploaded // or queued for upload but not yet uploaded Future removeQueuedLocalFiles(Set localIDs) async { + if (localIDs.isEmpty) { + _logger.finest("No local IDs provided for removal"); + return 0; + } + final db = await instance.sqliteAsyncDB; - final inParam = localIDs.map((id) => "'$id'").join(','); + final placeholders = List.filled(localIDs.length, '?').join(','); final r = await db.execute( ''' DELETE FROM $filesTable - WHERE $columnLocalID IN ($inParam) and (collectionID IS NULL || collectionID = -1) - and ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1); - ''', + WHERE $columnLocalID IN ($placeholders) + AND (collectionID IS NULL OR collectionID = -1) + AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1) + ''', + localIDs.toList(), ); + if (r.isNotEmpty) { _logger.warning( "Removed ${r.length} potential dups for already queued local files", @@ -998,7 +1006,6 @@ class FilesDB with SqlDbBase { } else { _logger.finest("No duplicate id found for queued/uploaded files"); } - return r.length; } From 79eff8aa5ad51846e8fd75008a46d23d26cbafaa Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Thu, 24 Jul 2025 17:22:31 +0530 Subject: [PATCH 21/29] Fix db query & swallow errow --- mobile/apps/photos/lib/db/files_db.dart | 37 ++++++-- .../services/sync/remote_sync_service.dart | 12 ++- mobile/apps/photos/pubspec.lock | 94 +++++++++---------- 3 files changed, 86 insertions(+), 57 deletions(-) diff --git a/mobile/apps/photos/lib/db/files_db.dart b/mobile/apps/photos/lib/db/files_db.dart index 657148a61a..1957f55179 100644 --- a/mobile/apps/photos/lib/db/files_db.dart +++ b/mobile/apps/photos/lib/db/files_db.dart @@ -988,25 +988,44 @@ class FilesDB with SqlDbBase { } final db = await instance.sqliteAsyncDB; - final placeholders = List.filled(localIDs.length, '?').join(','); - final r = await db.execute( - ''' + const batchSize = 10000; + int totalRemoved = 0; + + final localIDsList = localIDs.toList(); + + for (int i = 0; i < localIDsList.length; i += batchSize) { + final endIndex = (i + batchSize > localIDsList.length) + ? localIDsList.length + : i + batchSize; + + final batch = localIDsList.sublist(i, endIndex); + final placeholders = List.filled(batch.length, '?').join(','); + + final r = await db.execute( + ''' DELETE FROM $filesTable WHERE $columnLocalID IN ($placeholders) - AND (collectionID IS NULL OR collectionID = -1) + AND ($columnCollectionID IS NULL OR $columnCollectionID = -1) AND ($columnUploadedFileID IS NULL OR $columnUploadedFileID = -1) ''', - localIDs.toList(), - ); + batch, + ); - if (r.isNotEmpty) { + if (r.isNotEmpty) { + _logger + .fine("Batch ${(i ~/ batchSize) + 1}: Removed ${r.length} files"); + totalRemoved += r.length; + } + } + + if (totalRemoved > 0) { _logger.warning( - "Removed ${r.length} potential dups for already queued local files", + "Removed $totalRemoved potential dups for already queued local files", ); } else { _logger.finest("No duplicate id found for queued/uploaded files"); } - return r.length; + return totalRemoved; } Future> getLocalFileIDsForCollection(int collectionID) async { diff --git a/mobile/apps/photos/lib/services/sync/remote_sync_service.dart b/mobile/apps/photos/lib/services/sync/remote_sync_service.dart index d0e995a438..9e2f7f6aed 100644 --- a/mobile/apps/photos/lib/services/sync/remote_sync_service.dart +++ b/mobile/apps/photos/lib/services/sync/remote_sync_service.dart @@ -51,6 +51,10 @@ class RemoteSyncService { Completer? _existingSync; bool _isExistingSyncSilent = false; + // _hasCleanupStaleEntry is used to track if we have already cleaned up + // statle db entries in this sync session. + bool _hasCleanupStaleEntry = false; + static const kHasSyncedArchiveKey = "has_synced_archive"; /* This setting is used to maintain a list of local IDs for videos that the user has manually marked for upload, even if the global video upload setting is currently disabled. @@ -371,8 +375,13 @@ class RemoteSyncService { final Set alreadyClaimedLocalIDs = await _db.getLocalIDsMarkedForOrAlreadyUploaded(ownerID); localIDsToSync.removeAll(alreadyClaimedLocalIDs); - if (alreadyClaimedLocalIDs.isNotEmpty) { + if (alreadyClaimedLocalIDs.isNotEmpty && !_hasCleanupStaleEntry) { + try { await _db.removeQueuedLocalFiles(alreadyClaimedLocalIDs); + } catch(e, s) { + _logger.severe("removeQueuedLocalFiles failed",e,s); + + } } } @@ -442,6 +451,7 @@ class RemoteSyncService { // "force reload due to display new files" Bus.instance.fire(ForceReloadHomeGalleryEvent("newFilesDisplay")); } + _hasCleanupStaleEntry = true; } Future updateDeviceFolderSyncStatus( diff --git a/mobile/apps/photos/pubspec.lock b/mobile/apps/photos/pubspec.lock index a5d14c3829..f8cf5809fe 100644 --- a/mobile/apps/photos/pubspec.lock +++ b/mobile/apps/photos/pubspec.lock @@ -130,10 +130,10 @@ packages: dependency: "direct main" description: name: async - sha256: d2872f9c19731c2e5f10444b14686eb7cc85c76274bd6c16e1816bff9a3bab63 + sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" url: "https://pub.dev" source: hosted - version: "2.12.0" + version: "2.11.0" battery_info: dependency: "direct main" description: @@ -155,10 +155,10 @@ packages: dependency: transitive description: name: boolean_selector - sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" url: "https://pub.dev" source: hosted - version: "2.1.2" + version: "2.1.1" brotli: dependency: transitive description: @@ -268,10 +268,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.3.0" checked_yaml: dependency: transitive description: @@ -301,10 +301,10 @@ packages: dependency: transitive description: name: clock - sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf url: "https://pub.dev" source: hosted - version: "1.1.2" + version: "1.1.1" code_builder: dependency: transitive description: @@ -317,10 +317,10 @@ packages: dependency: "direct main" description: name: collection - sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + sha256: a1ace0a119f20aabc852d165077c036cd864315bd99b7eaa10a60100341941bf url: "https://pub.dev" source: hosted - version: "1.19.1" + version: "1.19.0" computer: dependency: "direct main" description: @@ -619,10 +619,10 @@ packages: dependency: transitive description: name: fake_async - sha256: "6a95e56b2449df2273fd8c45a662d6947ce1ebb7aafe80e550a3f68297f3cacc" + sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" url: "https://pub.dev" source: hosted - version: "1.3.2" + version: "1.3.1" fast_base58: dependency: "direct main" description: @@ -668,10 +668,10 @@ packages: dependency: transitive description: name: file - sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + sha256: "5fc22d7c25582e38ad9a8515372cd9a93834027aacf1801cf01164dac0ffa08c" url: "https://pub.dev" source: hosted - version: "7.0.1" + version: "7.0.0" file_saver: dependency: "direct main" description: @@ -1416,18 +1416,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: c35baad643ba394b40aac41080300150a4f08fd0fd6a10378f8f7c6bc161acec + sha256: "7bb2830ebd849694d1ec25bf1f44582d6ac531a57a365a803a6034ff751d2d06" url: "https://pub.dev" source: hosted - version: "10.0.8" + version: "10.0.7" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: f8b613e7e6a13ec79cfdc0e97638fddb3ab848452eff057653abd3edba760573 + sha256: "9491a714cca3667b60b5c420da8217e6de0d1ba7a5ec322fab01758f6998f379" url: "https://pub.dev" source: hosted - version: "3.0.9" + version: "3.0.8" leak_tracker_testing: dependency: transitive description: @@ -1552,10 +1552,10 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: d2323aa2060500f906aa31a895b4030b6da3ebdcc5619d14ce1aada65cd161cb url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.16+1" material_color_utilities: dependency: transitive description: @@ -1645,10 +1645,10 @@ packages: dependency: transitive description: name: meta - sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.16.0" + version: "1.15.0" mgrs_dart: dependency: transitive description: @@ -1859,10 +1859,10 @@ packages: dependency: "direct main" description: name: path - sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + sha256: "087ce49c3f0dc39180befefc60fdb4acd8f8620e5682fe2476afd0b3688bb4af" url: "https://pub.dev" source: hosted - version: "1.9.1" + version: "1.9.0" path_drawing: dependency: transitive description: @@ -2019,10 +2019,10 @@ packages: dependency: transitive description: name: platform - sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.6" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -2068,10 +2068,10 @@ packages: dependency: transitive description: name: process - sha256: "107d8be718f120bbba9dcd1e95e3bd325b1b4a4f07db64154635ba03f2567a0d" + sha256: "21e54fd2faf1b5bdd5102afd25012184a6793927648ea81eea80552ac9405b32" url: "https://pub.dev" source: hosted - version: "5.0.3" + version: "5.0.2" proj4dart: dependency: transitive description: @@ -2346,10 +2346,10 @@ packages: dependency: transitive description: name: source_span - sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" url: "https://pub.dev" source: hosted - version: "1.10.1" + version: "1.10.0" sprintf: dependency: transitive description: @@ -2434,10 +2434,10 @@ packages: dependency: transitive description: name: stack_trace - sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + sha256: "9f47fd3630d76be3ab26f0ee06d213679aa425996925ff3feffdec504931c377" url: "https://pub.dev" source: hosted - version: "1.12.1" + version: "1.12.0" step_progress_indicator: dependency: "direct main" description: @@ -2450,10 +2450,10 @@ packages: dependency: transitive description: name: stream_channel - sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + sha256: ba2aa5d8cc609d96bbb2899c28934f9e1af5cddbd60a827822ea467161eb54e7 url: "https://pub.dev" source: hosted - version: "2.1.4" + version: "2.1.2" stream_transform: dependency: transitive description: @@ -2466,10 +2466,10 @@ packages: dependency: transitive description: name: string_scanner - sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + sha256: "688af5ed3402a4bde5b3a6c15fd768dbf2621a614950b17f04626c431ab3c4c3" url: "https://pub.dev" source: hosted - version: "1.4.1" + version: "1.3.0" styled_text: dependency: "direct main" description: @@ -2522,34 +2522,34 @@ packages: dependency: transitive description: name: term_glyph - sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 url: "https://pub.dev" source: hosted - version: "1.2.2" + version: "1.2.1" test: dependency: "direct dev" description: name: test - sha256: "301b213cd241ca982e9ba50266bd3f5bd1ea33f1455554c5abb85d1be0e2d87e" + sha256: "713a8789d62f3233c46b4a90b174737b2c04cb6ae4500f2aa8b1be8f03f5e67f" url: "https://pub.dev" source: hosted - version: "1.25.15" + version: "1.25.8" test_api: dependency: transitive description: name: test_api - sha256: fb31f383e2ee25fbbfe06b40fe21e1e458d14080e3c67e7ba0acfde4df4e0bbd + sha256: "664d3a9a64782fcdeb83ce9c6b39e78fd2971d4e37827b9b06c3aa1edc5e760c" url: "https://pub.dev" source: hosted - version: "0.7.4" + version: "0.7.3" test_core: dependency: transitive description: name: test_core - sha256: "84d17c3486c8dfdbe5e12a50c8ae176d15e2a771b96909a9442b40173649ccaa" + sha256: "12391302411737c176b0b5d6491f466b0dd56d4763e347b6714efbaa74d7953d" url: "https://pub.dev" source: hosted - version: "0.6.8" + version: "0.6.5" thermal: dependency: "direct main" description: @@ -2813,10 +2813,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "0968250880a6c5fe7edc067ed0a13d4bae1577fe2771dcf3010d52c4a9d3ca14" + sha256: f6be3ed8bd01289b34d679c2b62226f63c0e69f9fd2e50a6b3c1c729a961041b url: "https://pub.dev" source: hosted - version: "14.3.1" + version: "14.3.0" volume_controller: dependency: transitive description: @@ -2978,5 +2978,5 @@ packages: source: hosted version: "3.1.3" sdks: - dart: ">=3.7.0-0 <4.0.0" + dart: ">=3.5.0 <4.0.0" flutter: ">=3.24.0" From 0d162b6075c30a27b33e1ca9fe12d839f62ea5eb Mon Sep 17 00:00:00 2001 From: Daniel T Date: Thu, 24 Jul 2025 15:02:48 -0500 Subject: [PATCH 22/29] chore: add accredible custom icon --- .../auth/assets/custom-icons/_data/custom-icons.json | 10 ++++++++++ .../apps/auth/assets/custom-icons/icons/accredible.svg | 8 ++++++++ 2 files changed, 18 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/accredible.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index ba42bb47f1..c748203add 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -10,6 +10,16 @@ { "title": "3Commas" }, + { + "title": "Accredible", + "slug": "accredible", + "altNames": [ + "Accredible Certificates", + "Accredible Badges", + "Digital Credentials", + "certificates.zaka.ai" + ] + }, { "title": "Addy.io", "slug": "addy_io" diff --git a/mobile/apps/auth/assets/custom-icons/icons/accredible.svg b/mobile/apps/auth/assets/custom-icons/icons/accredible.svg new file mode 100644 index 0000000000..707dbf482d --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/accredible.svg @@ -0,0 +1,8 @@ + + + + + + + + From d57daf91a038259ec6ff57919fdba7b62a7e7294 Mon Sep 17 00:00:00 2001 From: Daniel T Date: Thu, 24 Jul 2025 15:19:17 -0500 Subject: [PATCH 23/29] chore: add xvideos custom icon --- .../apps/auth/assets/custom-icons/_data/custom-icons.json | 8 ++++++++ mobile/apps/auth/assets/custom-icons/icons/xvideos.svg | 1 + 2 files changed, 9 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/xvideos.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index ba42bb47f1..4fad895b57 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -1581,6 +1581,14 @@ "title": "xAI", "slug": "xai" }, + { + "title": "XVideos", + "slug": "xvideos", + "altNames": [ + "X Videos", + "xvideos.com" + ] + }, { "title": "Cronometer", "slug": "cronometer" diff --git a/mobile/apps/auth/assets/custom-icons/icons/xvideos.svg b/mobile/apps/auth/assets/custom-icons/icons/xvideos.svg new file mode 100644 index 0000000000..91ccdbba27 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/xvideos.svg @@ -0,0 +1 @@ + \ No newline at end of file From bc3302157cd7eefc52c4122da5f5604a70090246 Mon Sep 17 00:00:00 2001 From: Daniel T Date: Thu, 24 Jul 2025 15:27:05 -0500 Subject: [PATCH 24/29] chore: add custom icon for chaturbate --- .../apps/auth/assets/custom-icons/_data/custom-icons.json | 7 +++++++ mobile/apps/auth/assets/custom-icons/icons/chaturbate.svg | 1 + 2 files changed, 8 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/chaturbate.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index ba42bb47f1..44c963dac5 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -279,6 +279,13 @@ "title": "CERN", "slug": "cern" }, + { + "title": "Chaturbate", + "slug": "chaturbate", + "altNames": [ + "Chaturbate.com" + ] + }, { "title": "ChangeNOW" }, diff --git a/mobile/apps/auth/assets/custom-icons/icons/chaturbate.svg b/mobile/apps/auth/assets/custom-icons/icons/chaturbate.svg new file mode 100644 index 0000000000..2c6992ebe7 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/chaturbate.svg @@ -0,0 +1 @@ + \ No newline at end of file From 3ca5303db633d9086c6f8587a3850a3756b4c9f7 Mon Sep 17 00:00:00 2001 From: Daniel T Date: Thu, 24 Jul 2025 15:31:04 -0500 Subject: [PATCH 25/29] chore: add custom icon for lifemiles --- .../custom-icons/_data/custom-icons.json | 9 ++ .../assets/custom-icons/icons/lifemiles.svg | 101 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/lifemiles.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index ba42bb47f1..a27ecc3cc7 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -742,6 +742,15 @@ { "title": "Letterboxd" }, + { + "title": "LifeMiles", + "slug": "lifemiles", + "altNames": [ + "Life Miles", + "lifemiles.com", + "Avianca LifeMiles" + ] + }, { "title": "lincolnfinancial", "altNames": [ diff --git a/mobile/apps/auth/assets/custom-icons/icons/lifemiles.svg b/mobile/apps/auth/assets/custom-icons/icons/lifemiles.svg new file mode 100644 index 0000000000..b08d6f0107 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/lifemiles.svg @@ -0,0 +1,101 @@ + + + +image/svg+xml + + + + + + + + + + + + + + + + + + + \ No newline at end of file From 25117f846a79bb04f59894e8091a3df8ef08567e Mon Sep 17 00:00:00 2001 From: Daniel T Date: Thu, 24 Jul 2025 15:34:51 -0500 Subject: [PATCH 26/29] chore: add custom icon for stripchat --- .../auth/assets/custom-icons/_data/custom-icons.json | 9 +++++++++ mobile/apps/auth/assets/custom-icons/icons/stripchat.svg | 1 + 2 files changed, 10 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/stripchat.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index ba42bb47f1..e5bc1c018e 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -1299,6 +1299,15 @@ "title": "Startmail", "slug": "startmail" }, + { + "title": "Stripchat", + "slug": "stripchat", + "altNames": [ + "Strip Chat", + "stripchat.com", + "StripChat Live" + ] + }, { "title": "STRATO", "hex": "FF8800" diff --git a/mobile/apps/auth/assets/custom-icons/icons/stripchat.svg b/mobile/apps/auth/assets/custom-icons/icons/stripchat.svg new file mode 100644 index 0000000000..4a338ee588 --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/stripchat.svg @@ -0,0 +1 @@ + \ No newline at end of file From 8f8eeb82a97972314eae13050644342159e8ba3c Mon Sep 17 00:00:00 2001 From: Daniel T Date: Thu, 24 Jul 2025 15:51:16 -0500 Subject: [PATCH 27/29] chore: add custom icon for auth digital --- .../apps/auth/assets/custom-icons/_data/custom-icons.json | 7 +++++++ .../apps/auth/assets/custom-icons/icons/auth_digital.svg | 1 + 2 files changed, 8 insertions(+) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/auth_digital.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index ba42bb47f1..11ee25eceb 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -71,6 +71,13 @@ ], "hex": "fd4b2d" }, + { + "title": "Autenticacion Digital", + "slug": "autenticacion-digital", + "altNames": [ + "autenticaciondigital.and.gov.co" + ] + }, { "title": "availity" }, diff --git a/mobile/apps/auth/assets/custom-icons/icons/auth_digital.svg b/mobile/apps/auth/assets/custom-icons/icons/auth_digital.svg new file mode 100644 index 0000000000..173f12977c --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/auth_digital.svg @@ -0,0 +1 @@ + \ No newline at end of file From 93b7cb8beacc431abfdb277706c864a2ae844f1f Mon Sep 17 00:00:00 2001 From: Eric Nielsen <4120606+ericbn@users.noreply.github.com> Date: Thu, 24 Jul 2025 21:55:15 -0500 Subject: [PATCH 28/29] Add custom icons for Tableau and X --- .../custom-icons/_data/custom-icons.json | 11 +++- .../assets/custom-icons/icons/tableau.svg | 52 +++++++++++++++++++ .../apps/auth/assets/custom-icons/icons/x.svg | 18 +++++++ 3 files changed, 80 insertions(+), 1 deletion(-) create mode 100644 mobile/apps/auth/assets/custom-icons/icons/tableau.svg create mode 100644 mobile/apps/auth/assets/custom-icons/icons/x.svg diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index ba42bb47f1..39a9b24bb5 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -436,7 +436,7 @@ "title": "emeritihealth", "altNames": [ "Emeriti Health", - "Emeriti Retirement Health", + "Emeriti Retirement Health" ] }, { @@ -1317,6 +1317,9 @@ "T-Mobile ID" ] }, + { + "title": "Tableau" + }, { "title": "TCPShield" }, @@ -1526,6 +1529,12 @@ { "title": "WYZE" }, + { + "title": "X", + "altNames": [ + "Twitter" + ] + }, { "title": "Xbox", "hex": "107C10" diff --git a/mobile/apps/auth/assets/custom-icons/icons/tableau.svg b/mobile/apps/auth/assets/custom-icons/icons/tableau.svg new file mode 100644 index 0000000000..361bb578ac --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/tableau.svg @@ -0,0 +1,52 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/mobile/apps/auth/assets/custom-icons/icons/x.svg b/mobile/apps/auth/assets/custom-icons/icons/x.svg new file mode 100644 index 0000000000..d6464cc0bf --- /dev/null +++ b/mobile/apps/auth/assets/custom-icons/icons/x.svg @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + \ No newline at end of file From 4d6d3d651a1552b4765b1f1cd7e89f71cb9b1941 Mon Sep 17 00:00:00 2001 From: Aman Raj Singh Mourya <146618155+AmanRajSinghMourya@users.noreply.github.com> Date: Fri, 25 Jul 2025 12:44:37 +0530 Subject: [PATCH 29/29] Minor Fix --- mobile/apps/auth/assets/custom-icons/_data/custom-icons.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json index e5bc1c018e..566176e3d2 100644 --- a/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json +++ b/mobile/apps/auth/assets/custom-icons/_data/custom-icons.json @@ -436,7 +436,7 @@ "title": "emeritihealth", "altNames": [ "Emeriti Health", - "Emeriti Retirement Health", + "Emeriti Retirement Health" ] }, {