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.
|
||||
|
||||
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/memories_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/utils/crypto_util.dart';
|
||||
import 'package:photos/utils/file_uploader.dart';
|
||||
|
@ -157,7 +158,9 @@ class Configuration {
|
|||
_cachedToken = null;
|
||||
_secretKey = null;
|
||||
await FilesDB.instance.clearTable();
|
||||
await ObjectBox.instance.clearTable();
|
||||
SemanticSearchService.instance.hasInitialized
|
||||
? await ObjectBox.instance.clearTable()
|
||||
: null;
|
||||
await CollectionsDB.instance.clearTable();
|
||||
await MemoriesDB.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 defaultCityRadius = 10.0;
|
||||
|
||||
const galleryGridSpacing = 2.0;
|
||||
|
||||
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_tag/location_tag.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";
|
||||
|
||||
class LocationService {
|
||||
late SharedPreferences prefs;
|
||||
final Logger _logger = Logger((LocationService).toString());
|
||||
final List<City> _cities = [];
|
||||
|
||||
LocationService._privateConstructor();
|
||||
|
||||
static final LocationService instance = LocationService._privateConstructor();
|
||||
|
||||
static const kCitiesRemotePath = "https://assets.ente.io/world_cities.json";
|
||||
|
||||
void init(SharedPreferences preferences) {
|
||||
prefs = preferences;
|
||||
if (FeatureFlagService.instance.isInternalUserOrDebugBuild()) {
|
||||
_loadCities();
|
||||
}
|
||||
}
|
||||
|
||||
Future<Iterable<LocalEntity<LocationTag>>> _getStoredLocationTags() async {
|
||||
|
@ -31,6 +39,10 @@ class LocationService {
|
|||
);
|
||||
}
|
||||
|
||||
List<City> getAllCities() {
|
||||
return _cities;
|
||||
}
|
||||
|
||||
Future<Iterable<LocalEntity<LocationTag>>> getLocationTags() {
|
||||
return _getStoredLocationTags();
|
||||
}
|
||||
|
@ -203,6 +215,52 @@ class LocationService {
|
|||
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 {
|
||||
|
|
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:intl/intl.dart";
|
||||
import 'package:logging/logging.dart';
|
||||
import "package:photos/core/constants.dart";
|
||||
import 'package:photos/core/event_bus.dart';
|
||||
import 'package:photos/data/holidays.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_type.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/search/album_search_result.dart';
|
||||
import 'package:photos/models/search/generic_search_result.dart';
|
||||
|
@ -681,6 +683,52 @@ class SearchService {
|
|||
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 {
|
||||
try {
|
||||
final Map<LocalEntity<LocationTag>, List<EnteFile>> tagToItemsMap = {};
|
||||
|
|
|
@ -46,6 +46,8 @@ class SemanticSearchService {
|
|||
Future<List<EnteFile>>? _ongoingRequest;
|
||||
PendingQuery? _nextQuery;
|
||||
|
||||
get hasInitialized => _hasInitialized;
|
||||
|
||||
Future<void> init() async {
|
||||
if (!LocalSettings.instance.hasEnabledMagicSearch()) {
|
||||
return;
|
||||
|
|
|
@ -278,7 +278,7 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
String query,
|
||||
) {
|
||||
int resultCount = 0;
|
||||
final maxResultCount = _isYearValid(query) ? 11 : 10;
|
||||
final maxResultCount = _isYearValid(query) ? 12 : 11;
|
||||
final streamController = StreamController<List<SearchResult>>();
|
||||
|
||||
if (query.isEmpty) {
|
||||
|
@ -286,113 +286,84 @@ class SearchWidgetState extends State<SearchWidget> {
|
|||
streamController.close();
|
||||
return streamController.stream;
|
||||
}
|
||||
|
||||
void onResultsReceived(List<SearchResult> results) {
|
||||
streamController.sink.add(results);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
}
|
||||
|
||||
if (_isYearValid(query)) {
|
||||
_searchService.getYearSearchResults(query).then((yearSearchResults) {
|
||||
streamController.sink.add(yearSearchResults);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(yearSearchResults);
|
||||
});
|
||||
}
|
||||
|
||||
_searchService.getHolidaySearchResults(context, query).then(
|
||||
(holidayResults) {
|
||||
streamController.sink.add(holidayResults);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(holidayResults);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getFileTypeResults(context, query).then(
|
||||
(fileTypeSearchResults) {
|
||||
streamController.sink.add(fileTypeSearchResults);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(fileTypeSearchResults);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getCaptionAndNameResults(query).then(
|
||||
(captionAndDisplayNameResult) {
|
||||
streamController.sink.add(captionAndDisplayNameResult);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(captionAndDisplayNameResult);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getFileExtensionResults(query).then(
|
||||
(fileExtnResult) {
|
||||
streamController.sink.add(fileExtnResult);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(fileExtnResult);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getLocationResults(query).then(
|
||||
(locationResult) {
|
||||
streamController.sink.add(locationResult);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(locationResult);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getCityResults(query).then(
|
||||
(results) {
|
||||
onResultsReceived(results);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getCollectionSearchResults(query).then(
|
||||
(collectionResults) {
|
||||
streamController.sink.add(collectionResults);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(collectionResults);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getMonthSearchResults(context, query).then(
|
||||
(monthResults) {
|
||||
streamController.sink.add(monthResults);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(monthResults);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getDateResults(context, query).then(
|
||||
(possibleEvents) {
|
||||
streamController.sink.add(possibleEvents);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(possibleEvents);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getMagicSearchResults(context, query).then(
|
||||
(magicResults) {
|
||||
streamController.sink.add(magicResults);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(magicResults);
|
||||
},
|
||||
);
|
||||
|
||||
_searchService.getContactSearchResults(query).then(
|
||||
(contactResults) {
|
||||
streamController.sink.add(contactResults);
|
||||
resultCount++;
|
||||
if (resultCount == maxResultCount) {
|
||||
streamController.close();
|
||||
}
|
||||
onResultsReceived(contactResults);
|
||||
},
|
||||
);
|
||||
|
||||
|
|
Loading…
Reference in a new issue