From b0319ff1633f75604cb6915622415d5479b67419 Mon Sep 17 00:00:00 2001 From: monsieurtanuki Date: Thu, 12 Sep 2024 10:04:31 +0200 Subject: [PATCH] feat: 5568 - optimized search for price locations Impacted files: * `app_en.arb`: added a label for "location broader search" * `app_fr.arb`: added a label for "location broader search" * `dao_osm_location.dart`: added columns osmKey and osmValue * `local_database.dart`: upgraded the version in order to add columns to location table * `location_list_supplier.dart`: added optional parameters for optimized search; added fields osmKey and osmValue * `location_query_model.dart`: now we use 2 suppliers - optimized and broader * `location_query_page.dart`: now display optimized results first, then broader results after a button click * `osm_location.dart`: added fields osmKey and osmValue --- .../data_models/location_list_supplier.dart | 21 +++++++-- .../lib/data_models/location_query_model.dart | 47 +++++++++++++------ .../lib/database/dao_osm_location.dart | 12 +++++ .../lib/database/local_database.dart | 2 +- packages/smooth_app/lib/l10n/app_en.arb | 1 + packages/smooth_app/lib/l10n/app_fr.arb | 1 + .../pages/locations/location_query_page.dart | 39 ++++++++++++--- .../lib/pages/locations/osm_location.dart | 12 +++++ 8 files changed, 109 insertions(+), 26 deletions(-) diff --git a/packages/smooth_app/lib/data_models/location_list_supplier.dart b/packages/smooth_app/lib/data_models/location_list_supplier.dart index 032e7a1a913..4eba1ac39e7 100644 --- a/packages/smooth_app/lib/data_models/location_list_supplier.dart +++ b/packages/smooth_app/lib/data_models/location_list_supplier.dart @@ -9,12 +9,22 @@ import 'package:smooth_app/query/product_query.dart'; class LocationListSupplier { LocationListSupplier( this.query, + this.optimizedSearch, ); + /// Query text. final String query; + /// True if we want to focus on shops. + final bool optimizedSearch; + + /// Locations as result. final List locations = []; + /// Returns additional query parameters. + String _getAdditionalParameters() => + optimizedSearch ? '&osm_tag=shop&osm_tag=amenity' : ''; + /// Returns null if OK, or the message error Future asyncLoad() async { // don't ask me why, but it looks like we need to explicitly set a language, @@ -35,10 +45,9 @@ class LocationListSupplier { scheme: 'https', host: 'photon.komoot.io', path: 'api', - queryParameters: { - 'q': query, - 'lang': getQueryLanguage().offTag, - }, + query: 'q=${Uri.encodeComponent(query)}' + '&lang=${getQueryLanguage().offTag}' + '${_getAdditionalParameters()}', ), ); if (response.statusCode != 200) { @@ -70,6 +79,8 @@ class LocationListSupplier { final String? countryCode = properties['countrycode']; final String? country = properties['country']; final String? postCode = properties['postcode']; + final String? osmKey = properties['osm_key']; + final String? osmValue = properties['osm_value']; final OsmLocation osmLocation = OsmLocation( osmId: osmId, osmType: osmType, @@ -81,6 +92,8 @@ class LocationListSupplier { street: street, country: country, countryCode: countryCode, + osmKey: osmKey, + osmValue: osmValue, ); locations.add(osmLocation); } diff --git a/packages/smooth_app/lib/data_models/location_query_model.dart b/packages/smooth_app/lib/data_models/location_query_model.dart index 1ec9b11b305..9e19bc131a2 100644 --- a/packages/smooth_app/lib/data_models/location_query_model.dart +++ b/packages/smooth_app/lib/data_models/location_query_model.dart @@ -4,9 +4,13 @@ import 'package:smooth_app/pages/locations/osm_location.dart'; import 'package:smooth_app/pages/product/common/loading_status.dart'; /// Location query model. +/// +/// We use 2 location suppliers: +/// * the first one optimized on shops, as it's what we want +/// * an optional one with no restrictions, in case OSM data is a bit clumsy class LocationQueryModel with ChangeNotifier { LocationQueryModel(this.query) { - _asyncLoad(notify: true); + _asyncLoad(_supplierOptimized); } final String query; @@ -15,39 +19,54 @@ class LocationQueryModel with ChangeNotifier { String? _loadingError; List displayedResults = []; + bool _isOptimized = true; + bool get isOptimized => _isOptimized; + bool isEmpty() => displayedResults.isEmpty; String? get loadingError => _loadingError; LoadingStatus get loadingStatus => _loadingStatus; - late final LocationListSupplier supplier = LocationListSupplier(query); + /// A location supplier focused on shops. + late final LocationListSupplier _supplierOptimized = + LocationListSupplier(query, true); + + /// A location supplier without restrictions. + late final LocationListSupplier _supplierBroader = + LocationListSupplier(query, false); - Future _asyncLoad({ - final bool notify = false, - final bool fromScratch = false, - }) async { + Future _asyncLoad(final LocationListSupplier supplier) async { _loadingStatus = LoadingStatus.LOADING; + notifyListeners(); _loadingError = await supplier.asyncLoad(); if (_loadingError != null) { _loadingStatus = LoadingStatus.ERROR; } else { - await _process(supplier.locations, fromScratch); + await _process(supplier.locations); _loadingStatus = LoadingStatus.LOADED; } - if (notify) { - notifyListeners(); - } + notifyListeners(); return _loadingStatus == LoadingStatus.LOADED; } + final Set _locationKeys = {}; + Future _process( final List locations, - final bool fromScratch, ) async { - if (fromScratch) { - displayedResults.clear(); + for (final OsmLocation location in locations) { + final String primaryKey = location.primaryKey; + if (_locationKeys.contains(primaryKey)) { + continue; + } + displayedResults.add(location); + _locationKeys.add(primaryKey); } - displayedResults.addAll(locations); _loadingStatus = LoadingStatus.LOADED; } + + Future loadMore() async { + _isOptimized = false; + _asyncLoad(_supplierBroader); + } } diff --git a/packages/smooth_app/lib/database/dao_osm_location.dart b/packages/smooth_app/lib/database/dao_osm_location.dart index b02e041f70c..8a4958b2637 100644 --- a/packages/smooth_app/lib/database/dao_osm_location.dart +++ b/packages/smooth_app/lib/database/dao_osm_location.dart @@ -20,6 +20,8 @@ class DaoOsmLocation extends AbstractSqlDao { static const String _columnPostCode = 'post_code'; static const String _columnCountry = 'country'; static const String _columnCountryCode = 'country_code'; + static const String _columnOsmKey = 'osm_key'; + static const String _columnOsmValue = 'osm_value'; static const String _columnLastAccess = 'last_access'; static const List _columns = [ @@ -33,6 +35,8 @@ class DaoOsmLocation extends AbstractSqlDao { _columnPostCode, _columnCountry, _columnCountryCode, + _columnOsmKey, + _columnOsmValue, _columnLastAccess, ]; @@ -58,6 +62,10 @@ class DaoOsmLocation extends AbstractSqlDao { ',PRIMARY KEY($_columnId,$_columnType) on conflict replace' ')'); } + if (oldVersion < 7) { + await db.execute('alter table $_table add column $_columnOsmKey TEXT'); + await db.execute('alter table $_table add column $_columnOsmValue TEXT'); + } } /// Deletes the [OsmLocation] that matches the key. @@ -100,6 +108,8 @@ class DaoOsmLocation extends AbstractSqlDao { _columnPostCode: osmLocation.postcode, _columnCountry: osmLocation.country, _columnCountryCode: osmLocation.countryCode, + _columnOsmKey: osmLocation.osmKey, + _columnOsmValue: osmLocation.osmValue, _columnLastAccess: LocalDatabase.nowInMillis(), }, ); @@ -122,6 +132,8 @@ class DaoOsmLocation extends AbstractSqlDao { postcode: row[_columnPostCode] as String?, country: row[_columnCountry] as String?, countryCode: row[_columnCountryCode] as String?, + osmKey: row[_columnOsmKey] as String?, + osmValue: row[_columnOsmValue] as String?, ); } } diff --git a/packages/smooth_app/lib/database/local_database.dart b/packages/smooth_app/lib/database/local_database.dart index b3542785dee..5d5d7abddda 100644 --- a/packages/smooth_app/lib/database/local_database.dart +++ b/packages/smooth_app/lib/database/local_database.dart @@ -68,7 +68,7 @@ class LocalDatabase extends ChangeNotifier { final String databasePath = join(databasesRootPath, 'smoothie.db'); final Database database = await openDatabase( databasePath, - version: 6, + version: 7, singleInstance: true, onUpgrade: _onUpgrade, ); diff --git a/packages/smooth_app/lib/l10n/app_en.arb b/packages/smooth_app/lib/l10n/app_en.arb index 8d605867752..db3fb6e6722 100644 --- a/packages/smooth_app/lib/l10n/app_en.arb +++ b/packages/smooth_app/lib/l10n/app_en.arb @@ -1911,6 +1911,7 @@ "prices_location_subtitle": "Shop", "prices_location_find": "Find a shop", "prices_location_mandatory": "You need to select a shop!", + "prices_location_search_broader": "Couldn't find what you were looking for? Let's try a broader search!", "prices_proof_subtitle": "Proof", "prices_proof_find": "Select a proof", "prices_proof_receipt": "Receipt", diff --git a/packages/smooth_app/lib/l10n/app_fr.arb b/packages/smooth_app/lib/l10n/app_fr.arb index cdbf9f4e4ae..704a0637a65 100644 --- a/packages/smooth_app/lib/l10n/app_fr.arb +++ b/packages/smooth_app/lib/l10n/app_fr.arb @@ -1899,6 +1899,7 @@ "prices_location_subtitle": "Magasin", "prices_location_find": "Chercher un magasin", "prices_location_mandatory": "Vous devez choisir un magasin !", + "prices_location_search_broader": "Vous n'avez pas trouvé ce que vous cherchiez ? Essayons une recherche plus large !", "prices_proof_subtitle": "Preuve", "prices_proof_find": "Choisir une preuve", "prices_proof_receipt": "Ticket de caisse", diff --git a/packages/smooth_app/lib/pages/locations/location_query_page.dart b/packages/smooth_app/lib/pages/locations/location_query_page.dart index 61524e27a5e..5ffc3ff143e 100644 --- a/packages/smooth_app/lib/pages/locations/location_query_page.dart +++ b/packages/smooth_app/lib/pages/locations/location_query_page.dart @@ -3,8 +3,10 @@ import 'package:flutter_gen/gen_l10n/app_localizations.dart'; import 'package:matomo_tracker/matomo_tracker.dart'; import 'package:provider/provider.dart'; import 'package:smooth_app/data_models/location_query_model.dart'; +import 'package:smooth_app/generic_lib/buttons/smooth_large_button_with_icon.dart'; import 'package:smooth_app/generic_lib/design_constants.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_back_button.dart'; +import 'package:smooth_app/generic_lib/widgets/smooth_card.dart'; import 'package:smooth_app/generic_lib/widgets/smooth_error_card.dart'; import 'package:smooth_app/pages/locations/search_location_preloaded_item.dart'; import 'package:smooth_app/pages/product/common/loading_status.dart'; @@ -67,7 +69,7 @@ class _LocationQueryPageState extends State } break; case LoadingStatus.LOADED: - if (_model.isEmpty()) { + if (_model.isEmpty() && !_model.isOptimized) { return SearchEmptyScreen( name: widget.query, emptiness: _getEmptyText( @@ -114,12 +116,35 @@ class _LocationQueryPageState extends State textColor: Theme.of(context).colorScheme.onSurface, ), child: ListView.builder( - itemBuilder: (BuildContext context, int index) => - SearchLocationPreloadedItem( - _model.displayedResults[index], - popFirst: true, - ).getWidget(context), - itemCount: _model.displayedResults.length, + itemBuilder: (BuildContext context, int index) { + if (index >= _model.displayedResults.length) { + if (_model.isOptimized) { + return SmoothCard( + child: SmoothLargeButtonWithIcon( + text: appLocalizations.prices_location_search_broader, + icon: Icons.search, + onPressed: () => _model.loadMore(), + ), + ); + } + return const Padding( + padding: EdgeInsets.only(top: SMALL_SPACE), + child: Center( + child: CircularProgressIndicator.adaptive(), + ), + ); + } + return SearchLocationPreloadedItem( + _model.displayedResults[index], + popFirst: true, + ).getWidget(context); + }, + itemCount: _model.displayedResults.length + + (_model.isOptimized + ? 1 + : _model.loadingStatus == LoadingStatus.LOADING + ? 1 + : 0), ), ), ); diff --git a/packages/smooth_app/lib/pages/locations/osm_location.dart b/packages/smooth_app/lib/pages/locations/osm_location.dart index 819e2391cbe..dd4742ed3ae 100644 --- a/packages/smooth_app/lib/pages/locations/osm_location.dart +++ b/packages/smooth_app/lib/pages/locations/osm_location.dart @@ -14,6 +14,8 @@ class OsmLocation { this.postcode, this.country, this.countryCode, + this.osmKey, + this.osmValue, }); final int osmId; @@ -26,6 +28,8 @@ class OsmLocation { final String? postcode; final String? country; final String? countryCode; + final String? osmKey; + final String? osmValue; LatLng getLatLng() => LatLng(latitude, longitude); @@ -65,9 +69,17 @@ class OsmLocation { } result.write(country); } + if (osmKey != null && osmValue != null) { + if (result.isNotEmpty) { + result.write(', '); + } + result.write('$osmKey:$osmValue'); + } if (result.isEmpty) { return null; } return result.toString(); } + + String get primaryKey => '${osmType.offTag}$osmId'; }