ソースを参照

Search: Support for search by calendar date & specific day

Neeraj Gupta 2 年 前
コミット
f7ed0a3db6

+ 3 - 0
lib/db/files_db.dart

@@ -603,6 +603,9 @@ class FilesDB {
     Set<int> ignoredCollectionIDs, {
     String order = 'ASC',
   }) async {
+    if (durations.isEmpty) {
+      return <File>[];
+    }
     final db = await instance.database;
     String whereClause = "( ";
     for (int index = 0; index < durations.length; index++) {

+ 118 - 25
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<List<File>> _cachedFilesFuture;
@@ -31,6 +33,7 @@ class SearchService {
   static const _maximumResultsLimit = 20;
 
   SearchService._privateConstructor();
+
   static final SearchService instance = SearchService._privateConstructor();
 
   Future<void> 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<List<GenericSearchResult>> getMonthSearchResults(String query) async {
     final List<GenericSearchResult> 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<List<GenericSearchResult>> getDateResults(
+    String query,
+  ) async {
+    final List<GenericSearchResult> 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<MonthData> _getMatchingMonths(String query) {
+    return allMonths
+        .where(
+          (monthData) =>
+              monthData.name.toLowerCase().startsWith(query.toLowerCase()),
+        )
+        .toList();
+  }
+
   Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
     return await FilesDB.instance.getFilesCreatedWithinDurations(
       [durationOfYear],
@@ -285,20 +320,30 @@ class SearchService {
     );
   }
 
-  List<List<int>> _getDurationsOfHolidayInEveryYear(int day, int month) {
+  List<List<int>> _getDurationsForCalendarDateInEveryYear(
+    int day,
+    int month, {
+    int year,
+  }) {
     final List<List<int>> 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<List<int>> _getDurationsOfMonthInEveryYear(int month) {
     final List<List<int>> 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<Tuple3<int, MonthData, int>> _getPossibleEventDate(String query) {
+    final List<Tuple3<int, MonthData, int>> 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<MonthData> potentialMonth =
+        resultCount > 1 ? _getMatchingMonths(result[1]) : allMonths;
+    final int parsedYear = resultCount >= 3 ? int.tryParse(result[2]) : null;
+    final List<int> 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;
+  }
 }

+ 40 - 28
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<SearchIconWidget> {
 
 class SearchWidget extends StatefulWidget {
   const SearchWidget({Key key}) : super(key: key);
+
   @override
   State<SearchWidget> createState() => _SearchWidgetState();
 }
@@ -57,6 +59,7 @@ class _SearchWidgetState extends State<SearchWidget> {
   final List<SearchResult> _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<SearchWidget> {
                         _query = value;
                         final List<SearchResult> 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<SearchWidget> {
       completer.complete(allResults);
       return;
     }
-    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);
+    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 monthResults = await _searchService.getMonthSearchResults(query);
-    allResults.addAll(monthResults);
     completer.complete(allResults);
   }
 

+ 25 - 0
lib/utils/date_time_util.dart

@@ -1,6 +1,8 @@
 import 'package:flutter/material.dart';
 import 'package:intl/intl.dart';
 
+const Set<int> monthWith31Days = {1, 3, 5, 7, 8, 10, 12};
+const Set<int> monthWith30Days = {4, 6, 9, 11};
 Map<int, String> _months = {
   1: "Jan",
   2: "Feb",
@@ -42,6 +44,7 @@ Map<int, String> _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;
+}