Merge branch 'main' into await_warnings
This commit is contained in:
commit
e24617cb96
|
@ -105,3 +105,7 @@ If you're interested in helping out with translation, please visit our [Crowdin
|
||||||
Follow us on [Twitter](https://twitter.com/enteio), join [r/enteio](https://reddit.com/r/enteio) or hang out on our [Discord](https://ente.io/discord) to get regular updates, connect with other customers, and discuss your ideas.
|
Follow us on [Twitter](https://twitter.com/enteio), join [r/enteio](https://reddit.com/r/enteio) or hang out on our [Discord](https://ente.io/discord) to get regular updates, connect with other customers, and discuss your ideas.
|
||||||
|
|
||||||
An important part of our journey is to build better software by consistently listening to community feedback. Please feel free to [share your thoughts](mailto:feedback@ente.io) with us at any time.
|
An important part of our journey is to build better software by consistently listening to community feedback. Please feel free to [share your thoughts](mailto:feedback@ente.io) with us at any time.
|
||||||
|
|
||||||
|
## 🙇 Attributions
|
||||||
|
|
||||||
|
- [Simple Maps](https://simplemaps.com/data/world-cities)
|
||||||
|
|
|
@ -27,6 +27,7 @@ import 'package:photos/services/favorites_service.dart';
|
||||||
import 'package:photos/services/ignored_files_service.dart';
|
import 'package:photos/services/ignored_files_service.dart';
|
||||||
import 'package:photos/services/memories_service.dart';
|
import 'package:photos/services/memories_service.dart';
|
||||||
import 'package:photos/services/search_service.dart';
|
import 'package:photos/services/search_service.dart';
|
||||||
|
import "package:photos/services/semantic_search/semantic_search_service.dart";
|
||||||
import 'package:photos/services/sync_service.dart';
|
import 'package:photos/services/sync_service.dart';
|
||||||
import 'package:photos/utils/crypto_util.dart';
|
import 'package:photos/utils/crypto_util.dart';
|
||||||
import 'package:photos/utils/file_uploader.dart';
|
import 'package:photos/utils/file_uploader.dart';
|
||||||
|
@ -157,7 +158,9 @@ class Configuration {
|
||||||
_cachedToken = null;
|
_cachedToken = null;
|
||||||
_secretKey = null;
|
_secretKey = null;
|
||||||
await FilesDB.instance.clearTable();
|
await FilesDB.instance.clearTable();
|
||||||
await ObjectBox.instance.clearTable();
|
SemanticSearchService.instance.hasInitialized
|
||||||
|
? await ObjectBox.instance.clearTable()
|
||||||
|
: null;
|
||||||
await CollectionsDB.instance.clearTable();
|
await CollectionsDB.instance.clearTable();
|
||||||
await MemoriesDB.instance.clearTable();
|
await MemoriesDB.instance.clearTable();
|
||||||
await PublicKeysDB.instance.clearTable();
|
await PublicKeysDB.instance.clearTable();
|
||||||
|
|
|
@ -61,6 +61,8 @@ const defaultRadiusValues = <double>[1, 2, 10, 20, 40, 80, 200, 400, 1200];
|
||||||
|
|
||||||
const defaultRadiusValue = 40.0;
|
const defaultRadiusValue = 40.0;
|
||||||
|
|
||||||
|
const defaultCityRadius = 10.0;
|
||||||
|
|
||||||
const galleryGridSpacing = 2.0;
|
const galleryGridSpacing = 2.0;
|
||||||
|
|
||||||
const searchSectionLimit = 7;
|
const searchSectionLimit = 7;
|
||||||
|
|
|
@ -10,18 +10,26 @@ import "package:photos/models/local_entity_data.dart";
|
||||||
import "package:photos/models/location/location.dart";
|
import "package:photos/models/location/location.dart";
|
||||||
import 'package:photos/models/location_tag/location_tag.dart';
|
import 'package:photos/models/location_tag/location_tag.dart';
|
||||||
import "package:photos/services/entity_service.dart";
|
import "package:photos/services/entity_service.dart";
|
||||||
|
import "package:photos/services/feature_flag_service.dart";
|
||||||
|
import "package:photos/services/remote_assets_service.dart";
|
||||||
import "package:shared_preferences/shared_preferences.dart";
|
import "package:shared_preferences/shared_preferences.dart";
|
||||||
|
|
||||||
class LocationService {
|
class LocationService {
|
||||||
late SharedPreferences prefs;
|
late SharedPreferences prefs;
|
||||||
final Logger _logger = Logger((LocationService).toString());
|
final Logger _logger = Logger((LocationService).toString());
|
||||||
|
final List<City> _cities = [];
|
||||||
|
|
||||||
LocationService._privateConstructor();
|
LocationService._privateConstructor();
|
||||||
|
|
||||||
static final LocationService instance = LocationService._privateConstructor();
|
static final LocationService instance = LocationService._privateConstructor();
|
||||||
|
|
||||||
|
static const kCitiesRemotePath = "https://assets.ente.io/world_cities.json";
|
||||||
|
|
||||||
void init(SharedPreferences preferences) {
|
void init(SharedPreferences preferences) {
|
||||||
prefs = preferences;
|
prefs = preferences;
|
||||||
|
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
|
||||||
|
_loadCities();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {
|
Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {
|
||||||
|
@ -31,6 +39,10 @@ class LocationService {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
List<City> getAllCities() {
|
||||||
|
return _cities;
|
||||||
|
}
|
||||||
|
|
||||||
Future<Iterable<LocalEntity<LocationTag>>> getLocationTags() {
|
Future<Iterable<LocalEntity<LocationTag>>> getLocationTags() {
|
||||||
return _getStoredLocationTags();
|
return _getStoredLocationTags();
|
||||||
}
|
}
|
||||||
|
@ -203,6 +215,52 @@ class LocationService {
|
||||||
rethrow;
|
rethrow;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<void> _loadCities() async {
|
||||||
|
try {
|
||||||
|
final data =
|
||||||
|
await RemoteAssetsService.instance.getAsset(kCitiesRemotePath);
|
||||||
|
final citiesJson = json.decode(await data.readAsString());
|
||||||
|
final List<dynamic> jsonData = citiesJson['data'];
|
||||||
|
final cities =
|
||||||
|
jsonData.map<City>((jsonItem) => City.fromMap(jsonItem)).toList();
|
||||||
|
_cities.clear();
|
||||||
|
_cities.addAll(cities);
|
||||||
|
_logger.info("Loaded cities");
|
||||||
|
} catch (e, s) {
|
||||||
|
_logger.severe("Failed to load cities", e, s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
class City {
|
||||||
|
final String city;
|
||||||
|
final String country;
|
||||||
|
final double lat;
|
||||||
|
final double lng;
|
||||||
|
|
||||||
|
City({
|
||||||
|
required this.city,
|
||||||
|
required this.country,
|
||||||
|
required this.lat,
|
||||||
|
required this.lng,
|
||||||
|
});
|
||||||
|
|
||||||
|
factory City.fromMap(Map<String, dynamic> map) {
|
||||||
|
return City(
|
||||||
|
city: map['city'] ?? '',
|
||||||
|
country: map['country'] ?? '',
|
||||||
|
lat: map['lat']?.toDouble() ?? 0.0,
|
||||||
|
lng: map['lng']?.toDouble() ?? 0.0,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
factory City.fromJson(String source) => City.fromMap(json.decode(source));
|
||||||
|
|
||||||
|
@override
|
||||||
|
String toString() {
|
||||||
|
return 'City(city: $city, country: $country, lat: $lat, lng: $lng)';
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
class GPSData {
|
class GPSData {
|
||||||
|
|
57
lib/services/remote_assets_service.dart
Normal file
57
lib/services/remote_assets_service.dart
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import "dart:io";
|
||||||
|
|
||||||
|
import "package:logging/logging.dart";
|
||||||
|
import "package:path_provider/path_provider.dart";
|
||||||
|
import "package:photos/core/network/network.dart";
|
||||||
|
|
||||||
|
class RemoteAssetsService {
|
||||||
|
static final _logger = Logger("RemoteAssetsService");
|
||||||
|
|
||||||
|
RemoteAssetsService._privateConstructor();
|
||||||
|
|
||||||
|
static final RemoteAssetsService instance =
|
||||||
|
RemoteAssetsService._privateConstructor();
|
||||||
|
|
||||||
|
Future<File> getAsset(String remotePath) async {
|
||||||
|
final path = await _getLocalPath(remotePath);
|
||||||
|
final file = File(path);
|
||||||
|
if (await file.exists()) {
|
||||||
|
_logger.info("Returning cached file for $remotePath");
|
||||||
|
return file;
|
||||||
|
} else {
|
||||||
|
final tempFile = File(path + ".temp");
|
||||||
|
await _downloadFile(remotePath, tempFile.path);
|
||||||
|
await tempFile.rename(path);
|
||||||
|
return File(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<String> _getLocalPath(String remotePath) async {
|
||||||
|
return (await getTemporaryDirectory()).path +
|
||||||
|
"/assets/" +
|
||||||
|
_urlToFileName(remotePath);
|
||||||
|
}
|
||||||
|
|
||||||
|
String _urlToFileName(String url) {
|
||||||
|
// Remove the protocol part (http:// or https://)
|
||||||
|
String fileName = url
|
||||||
|
.replaceAll(RegExp(r'https?://'), '')
|
||||||
|
// Replace all non-alphanumeric characters except for underscores and periods with an underscore
|
||||||
|
.replaceAll(RegExp(r'[^\w\.]'), '_');
|
||||||
|
// Optionally, you might want to trim the resulting string to a certain length
|
||||||
|
|
||||||
|
// Replace periods with underscores for better readability, if desired
|
||||||
|
fileName = fileName.replaceAll('.', '_');
|
||||||
|
|
||||||
|
return fileName;
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _downloadFile(String url, String savePath) async {
|
||||||
|
_logger.info("Downloading " + url);
|
||||||
|
final existingFile = File(savePath);
|
||||||
|
if (await existingFile.exists()) {
|
||||||
|
await existingFile.delete();
|
||||||
|
}
|
||||||
|
await NetworkClient.instance.getDio().download(url, savePath);
|
||||||
|
}
|
||||||
|
}
|
|
@ -3,6 +3,7 @@ import "dart:math";
|
||||||
import "package:flutter/cupertino.dart";
|
import "package:flutter/cupertino.dart";
|
||||||
import "package:intl/intl.dart";
|
import "package:intl/intl.dart";
|
||||||
import 'package:logging/logging.dart';
|
import 'package:logging/logging.dart';
|
||||||
|
import "package:photos/core/constants.dart";
|
||||||
import 'package:photos/core/event_bus.dart';
|
import 'package:photos/core/event_bus.dart';
|
||||||
import 'package:photos/data/holidays.dart';
|
import 'package:photos/data/holidays.dart';
|
||||||
import 'package:photos/data/months.dart';
|
import 'package:photos/data/months.dart';
|
||||||
|
@ -17,6 +18,7 @@ import "package:photos/models/file/extensions/file_props.dart";
|
||||||
import 'package:photos/models/file/file.dart';
|
import 'package:photos/models/file/file.dart';
|
||||||
import 'package:photos/models/file/file_type.dart';
|
import 'package:photos/models/file/file_type.dart';
|
||||||
import "package:photos/models/local_entity_data.dart";
|
import "package:photos/models/local_entity_data.dart";
|
||||||
|
import "package:photos/models/location/location.dart";
|
||||||
import "package:photos/models/location_tag/location_tag.dart";
|
import "package:photos/models/location_tag/location_tag.dart";
|
||||||
import 'package:photos/models/search/album_search_result.dart';
|
import 'package:photos/models/search/album_search_result.dart';
|
||||||
import 'package:photos/models/search/generic_search_result.dart';
|
import 'package:photos/models/search/generic_search_result.dart';
|
||||||
|
@ -681,6 +683,52 @@ class SearchService {
|
||||||
return searchResults;
|
return searchResults;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Future<List<GenericSearchResult>> getCityResults(String query) async {
|
||||||
|
final startTime = DateTime.now().microsecondsSinceEpoch;
|
||||||
|
final List<GenericSearchResult> searchResults = [];
|
||||||
|
final cities = LocationService.instance.getAllCities();
|
||||||
|
final matchingCities = <City>[];
|
||||||
|
final queryLower = query.toLowerCase();
|
||||||
|
for (City city in cities) {
|
||||||
|
if (city.city.toLowerCase().startsWith(queryLower)) {
|
||||||
|
matchingCities.add(city);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
final files = await getAllFiles();
|
||||||
|
final Map<City, List<EnteFile>> results = {};
|
||||||
|
for (final city in matchingCities) {
|
||||||
|
final List<EnteFile> matchingFiles = [];
|
||||||
|
final cityLocation = Location(latitude: city.lat, longitude: city.lng);
|
||||||
|
for (final file in files) {
|
||||||
|
if (file.hasLocation) {
|
||||||
|
if (LocationService.instance.isFileInsideLocationTag(
|
||||||
|
cityLocation,
|
||||||
|
file.location!,
|
||||||
|
defaultCityRadius,
|
||||||
|
)) {
|
||||||
|
matchingFiles.add(file);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (matchingFiles.isNotEmpty) {
|
||||||
|
results[city] = matchingFiles;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (final entry in results.entries) {
|
||||||
|
searchResults.add(
|
||||||
|
GenericSearchResult(
|
||||||
|
ResultType.location,
|
||||||
|
entry.key.city,
|
||||||
|
entry.value,
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
final endTime = DateTime.now().microsecondsSinceEpoch;
|
||||||
|
_logger
|
||||||
|
.info("Time taken " + ((endTime - startTime) / 1000).toString() + "ms");
|
||||||
|
return searchResults;
|
||||||
|
}
|
||||||
|
|
||||||
Future<List<GenericSearchResult>> getAllLocationTags(int? limit) async {
|
Future<List<GenericSearchResult>> getAllLocationTags(int? limit) async {
|
||||||
try {
|
try {
|
||||||
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
|
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
|
||||||
|
|
|
@ -46,6 +46,8 @@ class SemanticSearchService {
|
||||||
Future<List<EnteFile>>? _ongoingRequest;
|
Future<List<EnteFile>>? _ongoingRequest;
|
||||||
PendingQuery? _nextQuery;
|
PendingQuery? _nextQuery;
|
||||||
|
|
||||||
|
get hasInitialized => _hasInitialized;
|
||||||
|
|
||||||
Future<void> init() async {
|
Future<void> init() async {
|
||||||
if (!LocalSettings.instance.hasEnabledMagicSearch()) {
|
if (!LocalSettings.instance.hasEnabledMagicSearch()) {
|
||||||
return;
|
return;
|
||||||
|
|
|
@ -278,7 +278,7 @@ class SearchWidgetState extends State<SearchWidget> {
|
||||||
String query,
|
String query,
|
||||||
) {
|
) {
|
||||||
int resultCount = 0;
|
int resultCount = 0;
|
||||||
final maxResultCount = _isYearValid(query) ? 11 : 10;
|
final maxResultCount = _isYearValid(query) ? 12 : 11;
|
||||||
final streamController = StreamController<List<SearchResult>>();
|
final streamController = StreamController<List<SearchResult>>();
|
||||||
|
|
||||||
if (query.isEmpty) {
|
if (query.isEmpty) {
|
||||||
|
@ -286,113 +286,84 @@ class SearchWidgetState extends State<SearchWidget> {
|
||||||
streamController.close();
|
streamController.close();
|
||||||
return streamController.stream;
|
return streamController.stream;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void onResultsReceived(List<SearchResult> results) {
|
||||||
|
streamController.sink.add(results);
|
||||||
|
resultCount++;
|
||||||
|
if (resultCount == maxResultCount) {
|
||||||
|
streamController.close();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (_isYearValid(query)) {
|
if (_isYearValid(query)) {
|
||||||
_searchService.getYearSearchResults(query).then((yearSearchResults) {
|
_searchService.getYearSearchResults(query).then((yearSearchResults) {
|
||||||
streamController.sink.add(yearSearchResults);
|
onResultsReceived(yearSearchResults);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
_searchService.getHolidaySearchResults(context, query).then(
|
_searchService.getHolidaySearchResults(context, query).then(
|
||||||
(holidayResults) {
|
(holidayResults) {
|
||||||
streamController.sink.add(holidayResults);
|
onResultsReceived(holidayResults);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getFileTypeResults(context, query).then(
|
_searchService.getFileTypeResults(context, query).then(
|
||||||
(fileTypeSearchResults) {
|
(fileTypeSearchResults) {
|
||||||
streamController.sink.add(fileTypeSearchResults);
|
onResultsReceived(fileTypeSearchResults);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getCaptionAndNameResults(query).then(
|
_searchService.getCaptionAndNameResults(query).then(
|
||||||
(captionAndDisplayNameResult) {
|
(captionAndDisplayNameResult) {
|
||||||
streamController.sink.add(captionAndDisplayNameResult);
|
onResultsReceived(captionAndDisplayNameResult);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getFileExtensionResults(query).then(
|
_searchService.getFileExtensionResults(query).then(
|
||||||
(fileExtnResult) {
|
(fileExtnResult) {
|
||||||
streamController.sink.add(fileExtnResult);
|
onResultsReceived(fileExtnResult);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getLocationResults(query).then(
|
_searchService.getLocationResults(query).then(
|
||||||
(locationResult) {
|
(locationResult) {
|
||||||
streamController.sink.add(locationResult);
|
onResultsReceived(locationResult);
|
||||||
resultCount++;
|
},
|
||||||
if (resultCount == maxResultCount) {
|
);
|
||||||
streamController.close();
|
|
||||||
}
|
_searchService.getCityResults(query).then(
|
||||||
|
(results) {
|
||||||
|
onResultsReceived(results);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getCollectionSearchResults(query).then(
|
_searchService.getCollectionSearchResults(query).then(
|
||||||
(collectionResults) {
|
(collectionResults) {
|
||||||
streamController.sink.add(collectionResults);
|
onResultsReceived(collectionResults);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getMonthSearchResults(context, query).then(
|
_searchService.getMonthSearchResults(context, query).then(
|
||||||
(monthResults) {
|
(monthResults) {
|
||||||
streamController.sink.add(monthResults);
|
onResultsReceived(monthResults);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getDateResults(context, query).then(
|
_searchService.getDateResults(context, query).then(
|
||||||
(possibleEvents) {
|
(possibleEvents) {
|
||||||
streamController.sink.add(possibleEvents);
|
onResultsReceived(possibleEvents);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getMagicSearchResults(context, query).then(
|
_searchService.getMagicSearchResults(context, query).then(
|
||||||
(magicResults) {
|
(magicResults) {
|
||||||
streamController.sink.add(magicResults);
|
onResultsReceived(magicResults);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
_searchService.getContactSearchResults(query).then(
|
_searchService.getContactSearchResults(query).then(
|
||||||
(contactResults) {
|
(contactResults) {
|
||||||
streamController.sink.add(contactResults);
|
onResultsReceived(contactResults);
|
||||||
resultCount++;
|
|
||||||
if (resultCount == maxResultCount) {
|
|
||||||
streamController.close();
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue