2022-08-08 05:15:06 +00:00
|
|
|
import 'package:dio/dio.dart';
|
|
|
|
import 'package:logging/logging.dart';
|
|
|
|
import 'package:photos/core/configuration.dart';
|
2022-08-04 06:46:39 +00:00
|
|
|
import 'package:photos/core/event_bus.dart';
|
2022-08-08 05:15:06 +00:00
|
|
|
import 'package:photos/core/network.dart';
|
2022-08-16 06:54:05 +00:00
|
|
|
import 'package:photos/data/holidays.dart';
|
2022-08-17 10:50:40 +00:00
|
|
|
import 'package:photos/data/months.dart';
|
2022-08-22 05:18:28 +00:00
|
|
|
import 'package:photos/data/years.dart';
|
2022-08-04 06:46:39 +00:00
|
|
|
import 'package:photos/db/files_db.dart';
|
|
|
|
import 'package:photos/events/local_photos_updated_event.dart';
|
2022-08-09 15:09:18 +00:00
|
|
|
import 'package:photos/models/collection.dart';
|
|
|
|
import 'package:photos/models/collection_items.dart';
|
2022-08-04 06:46:39 +00:00
|
|
|
import 'package:photos/models/file.dart';
|
2022-08-08 05:15:06 +00:00
|
|
|
import 'package:photos/models/location.dart';
|
2022-08-16 11:49:58 +00:00
|
|
|
import 'package:photos/models/search/album_search_result.dart';
|
2022-08-12 11:08:26 +00:00
|
|
|
import 'package:photos/models/search/holiday_search_result.dart';
|
2022-08-10 13:27:04 +00:00
|
|
|
import 'package:photos/models/search/location_api_response.dart';
|
2022-08-08 05:15:06 +00:00
|
|
|
import 'package:photos/models/search/location_search_result.dart';
|
2022-08-17 10:50:40 +00:00
|
|
|
import 'package:photos/models/search/month_search_result.dart';
|
2022-08-16 11:49:58 +00:00
|
|
|
import 'package:photos/models/search/year_search_result.dart';
|
2022-08-09 15:09:18 +00:00
|
|
|
import 'package:photos/services/collections_service.dart';
|
2022-08-15 12:02:56 +00:00
|
|
|
import 'package:photos/utils/date_time_util.dart';
|
2022-08-04 06:46:39 +00:00
|
|
|
|
|
|
|
class SearchService {
|
2022-08-11 12:30:15 +00:00
|
|
|
Future<List<File>> _cachedFilesFuture;
|
2022-08-08 05:15:06 +00:00
|
|
|
final _dio = Network.instance.getDio();
|
|
|
|
final _config = Configuration.instance;
|
2022-08-11 14:38:43 +00:00
|
|
|
final _logger = Logger((SearchService).toString());
|
2022-08-09 15:09:18 +00:00
|
|
|
final _collectionService = CollectionsService.instance;
|
2022-08-10 10:09:56 +00:00
|
|
|
static const _maximumResultsLimit = 20;
|
2022-08-04 06:46:39 +00:00
|
|
|
|
|
|
|
SearchService._privateConstructor();
|
|
|
|
static final SearchService instance = SearchService._privateConstructor();
|
|
|
|
|
|
|
|
Future<void> init() async {
|
2022-08-06 12:35:29 +00:00
|
|
|
// Intention of delay is to give more CPU cycles to other tasks
|
|
|
|
Future.delayed(const Duration(seconds: 5), () async {
|
2022-08-10 13:27:04 +00:00
|
|
|
/* In case home screen loads before 5 seconds and user starts search,
|
|
|
|
future will not be null.So here getAllFiles won't run again in that case. */
|
2022-08-11 12:30:15 +00:00
|
|
|
if (_cachedFilesFuture == null) {
|
2022-08-10 13:27:04 +00:00
|
|
|
getAllFiles();
|
|
|
|
}
|
2022-08-06 12:35:29 +00:00
|
|
|
});
|
2022-08-04 06:46:39 +00:00
|
|
|
|
|
|
|
Bus.instance.on<LocalPhotosUpdatedEvent>().listen((event) {
|
2022-08-11 12:30:15 +00:00
|
|
|
_cachedFilesFuture = null;
|
2022-08-04 06:46:39 +00:00
|
|
|
getAllFiles();
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<File>> getAllFiles() async {
|
2022-08-11 12:30:15 +00:00
|
|
|
if (_cachedFilesFuture != null) {
|
|
|
|
return _cachedFilesFuture;
|
2022-08-06 13:44:52 +00:00
|
|
|
}
|
2022-08-11 12:30:15 +00:00
|
|
|
_cachedFilesFuture = FilesDB.instance.getAllFilesFromDB();
|
|
|
|
return _cachedFilesFuture;
|
2022-08-06 13:44:52 +00:00
|
|
|
}
|
|
|
|
|
2022-08-11 06:20:39 +00:00
|
|
|
Future<List<File>> getFileSearchResults(String query) async {
|
|
|
|
final List<File> fileSearchResults = [];
|
2022-08-10 04:48:40 +00:00
|
|
|
final List<File> files = await getAllFiles();
|
2022-08-10 13:27:04 +00:00
|
|
|
final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
|
2022-08-17 08:35:16 +00:00
|
|
|
for (var file in files) {
|
2022-08-11 06:20:39 +00:00
|
|
|
if (fileSearchResults.length >= _maximumResultsLimit) {
|
2022-08-11 05:40:29 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-08-10 13:27:04 +00:00
|
|
|
if (file.title.contains(nonCaseSensitiveRegexForQuery)) {
|
2022-08-11 06:20:39 +00:00
|
|
|
fileSearchResults.add(file);
|
2022-08-06 15:26:07 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-11 06:20:39 +00:00
|
|
|
return fileSearchResults;
|
2022-08-06 15:26:07 +00:00
|
|
|
}
|
|
|
|
|
2022-08-09 15:09:18 +00:00
|
|
|
void clearCache() {
|
2022-08-11 12:30:15 +00:00
|
|
|
_cachedFilesFuture = null;
|
2022-08-06 12:35:29 +00:00
|
|
|
}
|
2022-08-08 05:15:06 +00:00
|
|
|
|
2022-08-10 13:27:04 +00:00
|
|
|
Future<List<LocationSearchResult>> getLocationSearchResults(
|
2022-08-08 05:15:06 +00:00
|
|
|
String query,
|
|
|
|
) async {
|
2022-08-11 08:04:58 +00:00
|
|
|
final List<LocationSearchResult> locationSearchResults = [];
|
2022-08-08 05:15:06 +00:00
|
|
|
try {
|
2022-08-10 04:48:40 +00:00
|
|
|
final List<File> allFiles = await SearchService.instance.getAllFiles();
|
2022-08-09 14:40:50 +00:00
|
|
|
|
2022-08-08 05:15:06 +00:00
|
|
|
final response = await _dio.get(
|
|
|
|
_config.getHttpEndpoint() + "/search/location",
|
2022-08-11 11:48:09 +00:00
|
|
|
queryParameters: {"query": query, "limit": 10},
|
2022-08-08 05:15:06 +00:00
|
|
|
options: Options(
|
|
|
|
headers: {"X-Auth-Token": _config.getToken()},
|
|
|
|
),
|
|
|
|
);
|
|
|
|
|
2022-08-10 13:27:04 +00:00
|
|
|
final matchedLocationSearchResults =
|
|
|
|
LocationApiResponse.fromMap(response.data);
|
|
|
|
|
2022-08-17 08:35:16 +00:00
|
|
|
for (var locationData in matchedLocationSearchResults.results) {
|
2022-08-10 13:27:04 +00:00
|
|
|
final List<File> filesInLocation = [];
|
2022-08-09 12:23:43 +00:00
|
|
|
|
2022-08-17 08:35:16 +00:00
|
|
|
for (var file in allFiles) {
|
2022-08-10 13:27:04 +00:00
|
|
|
if (_isValidLocation(file.location) &&
|
|
|
|
_isLocationWithinBounds(file.location, locationData)) {
|
|
|
|
filesInLocation.add(file);
|
2022-08-08 05:15:06 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-11 07:44:42 +00:00
|
|
|
filesInLocation.sort(
|
|
|
|
(first, second) => second.creationTime.compareTo(first.creationTime),
|
|
|
|
);
|
2022-08-10 13:27:04 +00:00
|
|
|
if (filesInLocation.isNotEmpty) {
|
|
|
|
locationSearchResults.add(
|
|
|
|
LocationSearchResult(locationData.place, filesInLocation),
|
|
|
|
);
|
|
|
|
}
|
2022-08-08 05:15:06 +00:00
|
|
|
}
|
2022-08-11 08:04:58 +00:00
|
|
|
} catch (e) {
|
|
|
|
_logger.severe(e);
|
2022-08-08 05:15:06 +00:00
|
|
|
}
|
2022-08-11 08:04:58 +00:00
|
|
|
return locationSearchResults;
|
2022-08-08 05:15:06 +00:00
|
|
|
}
|
|
|
|
|
2022-08-09 15:09:18 +00:00
|
|
|
// getFilteredCollectionsWithThumbnail removes deleted or archived or
|
|
|
|
// collections which don't have a file from search result
|
2022-08-16 11:49:58 +00:00
|
|
|
Future<List<AlbumSearchResult>> getCollectionSearchResults(
|
2022-08-09 15:09:18 +00:00
|
|
|
String query,
|
|
|
|
) async {
|
2022-08-10 13:27:04 +00:00
|
|
|
final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
|
2022-08-11 05:15:42 +00:00
|
|
|
|
|
|
|
/*latestCollectionFiles is to identify collections which have at least one file as we don't display
|
|
|
|
empty collections and to get the file to pass for tumbnail */
|
2022-08-10 04:48:40 +00:00
|
|
|
final List<File> latestCollectionFiles =
|
2022-08-09 15:09:18 +00:00
|
|
|
await _collectionService.getLatestCollectionFiles();
|
2022-08-11 05:15:42 +00:00
|
|
|
|
2022-08-16 11:49:58 +00:00
|
|
|
final List<AlbumSearchResult> collectionSearchResults = [];
|
2022-08-11 05:15:42 +00:00
|
|
|
|
2022-08-17 08:35:16 +00:00
|
|
|
for (var file in latestCollectionFiles) {
|
2022-08-11 06:20:39 +00:00
|
|
|
if (collectionSearchResults.length >= _maximumResultsLimit) {
|
2022-08-11 05:40:29 +00:00
|
|
|
break;
|
|
|
|
}
|
2022-08-11 05:15:42 +00:00
|
|
|
final Collection collection =
|
|
|
|
CollectionsService.instance.getCollectionByID(file.collectionID);
|
|
|
|
if (!collection.isArchived() &&
|
|
|
|
collection.name.contains(nonCaseSensitiveRegexForQuery)) {
|
2022-08-16 11:49:58 +00:00
|
|
|
collectionSearchResults
|
|
|
|
.add(AlbumSearchResult(CollectionWithThumbnail(collection, file)));
|
2022-08-11 05:15:42 +00:00
|
|
|
}
|
2022-08-09 15:09:18 +00:00
|
|
|
}
|
2022-08-11 05:15:42 +00:00
|
|
|
|
2022-08-11 06:20:39 +00:00
|
|
|
return collectionSearchResults;
|
2022-08-09 15:09:18 +00:00
|
|
|
}
|
2022-08-10 13:27:04 +00:00
|
|
|
|
2022-08-22 05:18:28 +00:00
|
|
|
Future<List<YearSearchResult>> getYearSearchResults(
|
2022-08-22 06:02:06 +00:00
|
|
|
String yearFromQuery,
|
2022-08-22 05:18:28 +00:00
|
|
|
) async {
|
|
|
|
final List<YearSearchResult> yearSearchResults = [];
|
|
|
|
for (var yearData in YearsData.instance.yearsData) {
|
2022-08-22 06:02:06 +00:00
|
|
|
if (yearData.year.startsWith(yearFromQuery)) {
|
|
|
|
final List<File> filesInYear = await _getFilesInYear(yearData.duration);
|
2022-08-22 05:18:28 +00:00
|
|
|
if (filesInYear.isNotEmpty) {
|
|
|
|
yearSearchResults.add(
|
|
|
|
YearSearchResult(
|
|
|
|
yearData.year,
|
|
|
|
filesInYear,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
|
|
|
}
|
2022-08-16 11:49:58 +00:00
|
|
|
}
|
2022-08-22 05:18:28 +00:00
|
|
|
return yearSearchResults;
|
2022-08-11 13:22:30 +00:00
|
|
|
}
|
|
|
|
|
2022-08-15 14:03:42 +00:00
|
|
|
Future<List<HolidaySearchResult>> getHolidaySearchResults(
|
|
|
|
String query,
|
|
|
|
) async {
|
2022-08-17 10:50:40 +00:00
|
|
|
final List<HolidaySearchResult> holidaySearchResults = [];
|
2022-08-15 14:03:42 +00:00
|
|
|
|
|
|
|
final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
|
|
|
|
|
2022-08-17 08:35:16 +00:00
|
|
|
for (var holiday in allHolidays) {
|
2022-08-12 11:08:26 +00:00
|
|
|
if (holiday.name.contains(nonCaseSensitiveRegexForQuery)) {
|
2022-08-22 07:52:22 +00:00
|
|
|
final matchedFiles =
|
2022-08-16 07:13:21 +00:00
|
|
|
await FilesDB.instance.getFilesCreatedWithinDurations(
|
2022-08-22 07:52:22 +00:00
|
|
|
_getDurationsOfHolidayInEveryYear(holiday.day, holiday.month),
|
|
|
|
null,
|
|
|
|
order: 'DESC',
|
2022-08-15 12:02:56 +00:00
|
|
|
);
|
2022-08-22 07:52:22 +00:00
|
|
|
if (matchedFiles.isNotEmpty) {
|
|
|
|
holidaySearchResults.add(
|
|
|
|
HolidaySearchResult(holiday.name, matchedFiles),
|
|
|
|
);
|
|
|
|
}
|
2022-08-12 11:08:26 +00:00
|
|
|
}
|
|
|
|
}
|
2022-08-17 10:50:40 +00:00
|
|
|
return holidaySearchResults;
|
|
|
|
}
|
|
|
|
|
|
|
|
Future<List<MonthSearchResult>> getMonthSearchResults(String query) async {
|
|
|
|
final List<MonthSearchResult> monthSearchResults = [];
|
|
|
|
final nonCaseSensitiveRegexForQuery = RegExp(query, caseSensitive: false);
|
|
|
|
|
|
|
|
for (var month in allMonths) {
|
|
|
|
if (month.name.startsWith(nonCaseSensitiveRegexForQuery)) {
|
2022-08-22 11:40:27 +00:00
|
|
|
final matchedFiles =
|
2022-08-17 10:50:40 +00:00
|
|
|
await FilesDB.instance.getFilesCreatedWithinDurations(
|
2022-08-22 11:40:27 +00:00
|
|
|
_getDurationsOfMonthInEveryYear(month.monthNumber),
|
|
|
|
null,
|
|
|
|
order: 'DESC',
|
2022-08-17 10:50:40 +00:00
|
|
|
);
|
2022-08-22 11:40:27 +00:00
|
|
|
if (matchedFiles.isNotEmpty) {
|
|
|
|
monthSearchResults.add(
|
|
|
|
MonthSearchResult(
|
|
|
|
month.name,
|
|
|
|
matchedFiles,
|
|
|
|
),
|
|
|
|
);
|
|
|
|
}
|
2022-08-17 10:50:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return monthSearchResults;
|
2022-08-12 11:08:26 +00:00
|
|
|
}
|
|
|
|
|
2022-08-22 05:18:28 +00:00
|
|
|
Future<List<File>> _getFilesInYear(List<int> durationOfYear) async {
|
|
|
|
return await FilesDB.instance.getFilesCreatedWithinDurations(
|
|
|
|
[durationOfYear],
|
|
|
|
null,
|
|
|
|
order: "DESC",
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-08-16 07:13:21 +00:00
|
|
|
List<List<int>> _getDurationsOfHolidayInEveryYear(int day, int month) {
|
2022-08-16 11:49:58 +00:00
|
|
|
final List<List<int>> durationsOfHolidayInEveryYear = [];
|
2022-08-19 05:05:21 +00:00
|
|
|
for (var year = 1970; year <= currentYear; year++) {
|
2022-08-16 11:49:58 +00:00
|
|
|
durationsOfHolidayInEveryYear.add([
|
2022-08-19 05:05:21 +00:00
|
|
|
DateTime(year, month, day).microsecondsSinceEpoch,
|
|
|
|
DateTime(year, month, day + 1).microsecondsSinceEpoch,
|
2022-08-15 12:02:56 +00:00
|
|
|
]);
|
|
|
|
}
|
2022-08-16 11:49:58 +00:00
|
|
|
return durationsOfHolidayInEveryYear;
|
2022-08-15 12:02:56 +00:00
|
|
|
}
|
|
|
|
|
2022-08-17 10:50:40 +00:00
|
|
|
List<List<int>> _getDurationsOfMonthInEveryYear(int month) {
|
|
|
|
final List<List<int>> durationsOfMonthInEveryYear = [];
|
|
|
|
for (var year = 1970; year < currentYear; year++) {
|
|
|
|
durationsOfMonthInEveryYear.add([
|
|
|
|
DateTime.utc(year, month, 1).microsecondsSinceEpoch,
|
2022-08-17 11:48:25 +00:00
|
|
|
month == 12
|
2022-08-19 05:05:21 +00:00
|
|
|
? DateTime(year + 1, 1, 1).microsecondsSinceEpoch
|
|
|
|
: DateTime(year, month + 1, 1).microsecondsSinceEpoch,
|
2022-08-17 10:50:40 +00:00
|
|
|
]);
|
|
|
|
}
|
|
|
|
return durationsOfMonthInEveryYear;
|
|
|
|
}
|
|
|
|
|
2022-08-10 13:27:04 +00:00
|
|
|
bool _isValidLocation(Location location) {
|
|
|
|
return location != null &&
|
|
|
|
location.latitude != null &&
|
|
|
|
location.latitude != 0 &&
|
|
|
|
location.longitude != null &&
|
|
|
|
location.longitude != 0;
|
|
|
|
}
|
|
|
|
|
|
|
|
bool _isLocationWithinBounds(
|
|
|
|
Location location,
|
|
|
|
LocationDataFromResponse locationData,
|
|
|
|
) {
|
|
|
|
//format returned by the api is [lng,lat,lng,lat] where indexes 0 & 1 are southwest and 2 & 3 northeast
|
|
|
|
return location.longitude > locationData.bbox[0] &&
|
|
|
|
location.latitude > locationData.bbox[1] &&
|
|
|
|
location.longitude < locationData.bbox[2] &&
|
|
|
|
location.latitude < locationData.bbox[3];
|
|
|
|
}
|
2022-08-04 06:46:39 +00:00
|
|
|
}
|