Merge pull request #495 from ente-io/daySearch
Search: Support for search by calendar date & specific day
This commit is contained in:
commit
de9fefc9b2
|
@ -603,10 +603,13 @@ 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++) {
|
||||
whereClause += "($columnCreationTime > " +
|
||||
whereClause += "($columnCreationTime >= " +
|
||||
durations[index][0].toString() +
|
||||
" AND $columnCreationTime < " +
|
||||
durations[index][1].toString() +
|
||||
|
|
|
@ -21,6 +21,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 +32,7 @@ class SearchService {
|
|||
static const _maximumResultsLimit = 20;
|
||||
|
||||
SearchService._privateConstructor();
|
||||
|
||||
static final SearchService instance = SearchService._privateConstructor();
|
||||
|
||||
Future<void> init() async {
|
||||
|
@ -180,7 +182,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 +255,61 @@ 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 +318,28 @@ 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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
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 +368,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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -209,8 +209,7 @@ class _LandingPageWidgetState extends State<LandingPageWidget> {
|
|||
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",
|
||||
);
|
||||
|
|
|
@ -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) {
|
||||
|
@ -84,7 +87,7 @@ class _SearchWidgetState extends State<SearchWidget> {
|
|||
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,
|
||||
|
@ -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);
|
||||
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);
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue