From f7ed0a3db63694b04dfe4562ad0557f202d765f5 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 16 Sep 2022 17:41:16 +0530 Subject: [PATCH 1/5] Search: Support for search by calendar date & specific day --- lib/db/files_db.dart | 3 + lib/services/search_service.dart | 145 +++++++++++++++++++----- lib/ui/viewer/search/search_widget.dart | 68 ++++++----- lib/utils/date_time_util.dart | 25 ++++ 4 files changed, 187 insertions(+), 54 deletions(-) diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 88c279c17..6cb0f3408 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -603,6 +603,9 @@ class FilesDB { Set ignoredCollectionIDs, { String order = 'ASC', }) async { + if (durations.isEmpty) { + return []; + } final db = await instance.database; String whereClause = "( "; for (int index = 0; index < durations.length; index++) { diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 825248728..79391726c 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -1,6 +1,7 @@ // @dart=2.9 import 'package:dio/dio.dart'; +import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; @@ -21,6 +22,7 @@ import 'package:photos/models/search/location_api_response.dart'; import 'package:photos/models/search/search_result.dart'; import 'package:photos/services/collections_service.dart'; import 'package:photos/utils/date_time_util.dart'; +import 'package:tuple/tuple.dart'; class SearchService { Future> _cachedFilesFuture; @@ -31,6 +33,7 @@ class SearchService { static const _maximumResultsLimit = 20; SearchService._privateConstructor(); + static final SearchService instance = SearchService._privateConstructor(); Future init() async { @@ -180,7 +183,7 @@ class SearchService { if (holiday.name.toLowerCase().contains(query.toLowerCase())) { final matchedFiles = await FilesDB.instance.getFilesCreatedWithinDurations( - _getDurationsOfHolidayInEveryYear(holiday.day, holiday.month), + _getDurationsForCalendarDateInEveryYear(holiday.day, holiday.month), null, order: 'DESC', ); @@ -253,30 +256,62 @@ class SearchService { Future> getMonthSearchResults(String query) async { final List searchResults = []; - - for (var month in allMonths) { - if (month.name.toLowerCase().startsWith(query.toLowerCase())) { - final matchedFiles = - await FilesDB.instance.getFilesCreatedWithinDurations( - _getDurationsOfMonthInEveryYear(month.monthNumber), - null, - order: 'DESC', + for (var month in _getMatchingMonths(query)) { + final matchedFiles = + await FilesDB.instance.getFilesCreatedWithinDurations( + _getDurationsOfMonthInEveryYear(month.monthNumber), + null, + order: 'DESC', + ); + if (matchedFiles.isNotEmpty) { + searchResults.add( + GenericSearchResult( + ResultType.month, + month.name, + matchedFiles, + ), ); - if (matchedFiles.isNotEmpty) { - searchResults.add( - GenericSearchResult( - ResultType.month, - month.name, - matchedFiles, - ), - ); - } } } - return searchResults; } + Future> getDateResults( + String query, + ) async { + final List searchResults = []; + final potentialDates = _getPossibleEventDate(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), + null, + order: 'DESC', + ); + if (matchedFiles.isNotEmpty) { + searchResults.add( + GenericSearchResult(ResultType.event, + '$day ${potentialDate.item2.name} ${year ?? ''}', matchedFiles), + ); + } + } + return searchResults; + } + + List _getMatchingMonths(String query) { + return allMonths + .where( + (monthData) => + monthData.name.toLowerCase().startsWith(query.toLowerCase()), + ) + .toList(); + } + Future> _getFilesInYear(List durationOfYear) async { return await FilesDB.instance.getFilesCreatedWithinDurations( [durationOfYear], @@ -285,20 +320,30 @@ class SearchService { ); } - List> _getDurationsOfHolidayInEveryYear(int day, int month) { + List> _getDurationsForCalendarDateInEveryYear( + int day, + int month, { + int year, + }) { final List> durationsOfHolidayInEveryYear = []; - for (var year = 1970; year <= currentYear; year++) { - durationsOfHolidayInEveryYear.add([ - DateTime(year, month, day).microsecondsSinceEpoch, - DateTime(year, month, day + 1).microsecondsSinceEpoch, - ]); + final int startYear = year ?? searchStartYear; + final int endYear = year ?? currentYear; + for (var yr = startYear; yr <= endYear; yr++) { + if (isValidDate(day: day, month: month, year: yr)) { + durationsOfHolidayInEveryYear.add([ + DateTime(yr, month, day).microsecondsSinceEpoch, + DateTime(yr, month, day + 1).microsecondsSinceEpoch, + ]); + } else { + debugPrint("Invalid date $day : m: $month : y: $yr"); + } } return durationsOfHolidayInEveryYear; } List> _getDurationsOfMonthInEveryYear(int month) { final List> durationsOfMonthInEveryYear = []; - for (var year = 1970; year < currentYear; year++) { + for (var year = searchStartYear; year < currentYear; year++) { durationsOfMonthInEveryYear.add([ DateTime.utc(year, month, 1).microsecondsSinceEpoch, month == 12 @@ -327,4 +372,52 @@ class SearchService { location.longitude < locationData.bbox[2] && location.latitude < locationData.bbox[3]; } + + List> _getPossibleEventDate(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(result[1]) : allMonths; + 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; + } } diff --git a/lib/ui/viewer/search/search_widget.dart b/lib/ui/viewer/search/search_widget.dart index c50b020b8..c83e2596d 100644 --- a/lib/ui/viewer/search/search_widget.dart +++ b/lib/ui/viewer/search/search_widget.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; +import 'package:logging/logging.dart'; import 'package:photos/ente_theme_data.dart'; import 'package:photos/models/search/search_result.dart'; import 'package:photos/services/feature_flag_service.dart'; @@ -48,6 +49,7 @@ class _SearchIconWidgetState extends State { class SearchWidget extends StatefulWidget { const SearchWidget({Key key}) : super(key: key); + @override State createState() => _SearchWidgetState(); } @@ -57,6 +59,7 @@ class _SearchWidgetState extends State { final List _results = []; final _searchService = SearchService.instance; final _debouncer = Debouncer(const Duration(milliseconds: 100)); + final Logger _logger = Logger((_SearchWidgetState).toString()); @override Widget build(BuildContext context) { @@ -125,7 +128,7 @@ class _SearchWidgetState extends State { _query = value; final List allResults = await getSearchResultsForQuery(value); - /*checking if _query == value to make sure that the results are from the current query + /*checking if _query == value to make sure that the results are from the current query and not from the previous query (race condition).*/ if (mounted && _query == value) { setState(() { @@ -178,34 +181,43 @@ class _SearchWidgetState extends State { completer.complete(allResults); return; } - if (_isYearValid(query)) { - final yearResults = await _searchService.getYearSearchResults(query); - allResults.addAll(yearResults); + try { + if (_isYearValid(query)) { + final yearResults = await _searchService.getYearSearchResults(query); + allResults.addAll(yearResults); + } + + final holidayResults = + await _searchService.getHolidaySearchResults(query); + allResults.addAll(holidayResults); + + final fileTypeSearchResults = + await _searchService.getFileTypeResults(query); + allResults.addAll(fileTypeSearchResults); + + final fileExtnResult = + await _searchService.getFileExtensionResults(query); + allResults.addAll(fileExtnResult); + + final collectionResults = + await _searchService.getCollectionSearchResults(query); + allResults.addAll(collectionResults); + + if (FeatureFlagService.instance.isInternalUserOrDebugBuild() && + query.startsWith("l:")) { + final locationResults = await _searchService + .getLocationSearchResults(query.replaceAll("l:", "")); + allResults.addAll(locationResults); + } + + final monthResults = await _searchService.getMonthSearchResults(query); + allResults.addAll(monthResults); + + final possibleEvents = await _searchService.getDateResults(query); + allResults.addAll(possibleEvents); + } catch (e, s) { + _logger.severe("error during search", e, s); } - - final holidayResults = await _searchService.getHolidaySearchResults(query); - allResults.addAll(holidayResults); - - final fileTypeSearchResults = - await _searchService.getFileTypeResults(query); - allResults.addAll(fileTypeSearchResults); - - final fileExtnResult = await _searchService.getFileExtensionResults(query); - allResults.addAll(fileExtnResult); - - final collectionResults = - await _searchService.getCollectionSearchResults(query); - allResults.addAll(collectionResults); - - if (FeatureFlagService.instance.isInternalUserOrDebugBuild() && - query.startsWith("l:")) { - final locationResults = await _searchService - .getLocationSearchResults(query.replaceAll("l:", "")); - allResults.addAll(locationResults); - } - - final monthResults = await _searchService.getMonthSearchResults(query); - allResults.addAll(monthResults); completer.complete(allResults); } diff --git a/lib/utils/date_time_util.dart b/lib/utils/date_time_util.dart index 12c656af5..13688ebd9 100644 --- a/lib/utils/date_time_util.dart +++ b/lib/utils/date_time_util.dart @@ -1,6 +1,8 @@ import 'package:flutter/material.dart'; import 'package:intl/intl.dart'; +const Set monthWith31Days = {1, 3, 5, 7, 8, 10, 12}; +const Set monthWith30Days = {4, 6, 9, 11}; Map _months = { 1: "Jan", 2: "Feb", @@ -42,6 +44,7 @@ Map _days = { }; final currentYear = int.parse(DateTime.now().year.toString()); +const searchStartYear = 1970; //Jun 2022 String getMonthAndYear(DateTime dateTime) { @@ -243,3 +246,25 @@ String secondsToHHMMSS(int value) { return result; } + +bool isValidDate({ + required int day, + required int month, + required int year, +}) { + if (day < 0 || day > 31 || month < 0 || month > 12 || year < 0) { + return false; + } + if (monthWith30Days.contains(month) && day > 30) { + return false; + } + if (month == 2) { + if (day > 29) { + return false; + } + if (day == 29 && year % 4 != 0) { + return false; + } + } + return true; +} From 5059a4f102788b11ba0a6ec56f780b2ab2ee8433 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 16 Sep 2022 17:45:10 +0530 Subject: [PATCH 2/5] remove debugLogs --- lib/services/search_service.dart | 3 --- 1 file changed, 3 deletions(-) diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 79391726c..9169368de 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -1,7 +1,6 @@ // @dart=2.9 import 'package:dio/dio.dart'; -import 'package:flutter/foundation.dart'; import 'package:logging/logging.dart'; import 'package:photos/core/configuration.dart'; import 'package:photos/core/event_bus.dart'; @@ -334,8 +333,6 @@ class SearchService { DateTime(yr, month, day).microsecondsSinceEpoch, DateTime(yr, month, day + 1).microsecondsSinceEpoch, ]); - } else { - debugPrint("Invalid date $day : m: $month : y: $yr"); } } return durationsOfHolidayInEveryYear; From c4eb36188c2b66900dfa7659abf8728de83abc49 Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Fri, 16 Sep 2022 21:20:00 +0530 Subject: [PATCH 3/5] bug fix --- lib/db/files_db.dart | 2 +- lib/services/search_service.dart | 7 +++---- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/lib/db/files_db.dart b/lib/db/files_db.dart index 6cb0f3408..5b052a52a 100644 --- a/lib/db/files_db.dart +++ b/lib/db/files_db.dart @@ -609,7 +609,7 @@ class FilesDB { final db = await instance.database; String whereClause = "( "; for (int index = 0; index < durations.length; index++) { - whereClause += "($columnCreationTime > " + + whereClause += "($columnCreationTime >= " + durations[index][0].toString() + " AND $columnCreationTime < " + durations[index][1].toString() + diff --git a/lib/services/search_service.dart b/lib/services/search_service.dart index 9169368de..59c4f7da8 100644 --- a/lib/services/search_service.dart +++ b/lib/services/search_service.dart @@ -285,7 +285,6 @@ class SearchService { 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), @@ -340,7 +339,7 @@ class SearchService { List> _getDurationsOfMonthInEveryYear(int month) { final List> durationsOfMonthInEveryYear = []; - for (var year = searchStartYear; year < currentYear; year++) { + for (var year = searchStartYear; year <= currentYear; year++) { durationsOfMonthInEveryYear.add([ DateTime.utc(year, month, 1).microsecondsSinceEpoch, month == 12 @@ -396,13 +395,13 @@ class SearchService { final List matchingYears = []; if (parsedYear != null) { bool foundMatch = false; - for (int i = searchStartYear; i < currentYear; i++) { + 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) { + if (!foundMatch && parsedYear > 1000 && parsedYear <= currentYear) { matchingYears.add(parsedYear); } } From 0761860d76dfcc39cadb3a65bdab5d1d3899ab3b Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 17 Sep 2022 10:00:04 +0530 Subject: [PATCH 4/5] Fix copy --- lib/ui/landing_page_widget.dart | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/lib/ui/landing_page_widget.dart b/lib/ui/landing_page_widget.dart index aa75ce3d1..17fd35862 100644 --- a/lib/ui/landing_page_widget.dart +++ b/lib/ui/landing_page_widget.dart @@ -209,8 +209,7 @@ class _LandingPageWidgetState extends State { final result = await showChoiceDialog( context, "Please login again", - '''Unfortunately, the ente app had to log you out because of some - technical issues. Sorry!\n\nPlease login again.''', + '''Unfortunately, the ente app had to log you out because of some technical issues. Sorry!\n\nPlease login again.''', firstAction: "Cancel", secondAction: "Login", ); From 78fe978f59c2dda9cddfa9c87bbb82cb6120097a Mon Sep 17 00:00:00 2001 From: Neeraj Gupta <254676+ua741@users.noreply.github.com> Date: Sat, 17 Sep 2022 10:32:43 +0530 Subject: [PATCH 5/5] Search:update placeholder text --- lib/ui/viewer/search/search_widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ui/viewer/search/search_widget.dart b/lib/ui/viewer/search/search_widget.dart index c83e2596d..cc15e6d87 100644 --- a/lib/ui/viewer/search/search_widget.dart +++ b/lib/ui/viewer/search/search_widget.dart @@ -87,7 +87,7 @@ class _SearchWidgetState extends State { keyboardType: TextInputType.visiblePassword, // Above parameters are to disable auto-suggestion decoration: InputDecoration( - hintText: "Places, moments, albums...", + hintText: "Albums, months, days, years, ...", filled: true, contentPadding: const EdgeInsets.symmetric( horizontal: 16,