#1211 receiving geo: uri while editing location fills in coordinates

This commit is contained in:
Thibault Deckers 2024-10-03 00:36:40 +02:00
parent ec59e348c5
commit d859887319
6 changed files with 104 additions and 29 deletions

View file

@ -6,9 +6,17 @@ All notable changes to this project will be documented in this file.
### Added
- Map: OpenTopoMap layer
- Map: OpenTopoMap raster layer
- Map: OSM Liberty vector layer (hosted by OSM Americana)
- Interoperability: receiving `geo:` URI generally opens map page at location
- Interoperability: receiving `geo:` URI when editing item location fills in coordinates
- Map basic app shortcut
- Enterprise: support for work profile switching from the drawer
### Removed
- `Safe mode` basic app shortcut
## <a id="v1.11.13"></a>[v1.11.13] - 2024-09-17
### Added

26
lib/geo/uri.dart Normal file
View file

@ -0,0 +1,26 @@
import 'package:latlong2/latlong.dart';
// e.g. `geo:44.4361283,26.1027248?z=4.0(Bucharest)`
// cf https://en.wikipedia.org/wiki/Geo_URI_scheme
// cf https://developer.android.com/guide/components/intents-common#ViewMap
(LatLng, double?)? parseGeoUri(String? uri) {
if (uri != null) {
final geoUri = Uri.tryParse(uri);
if (geoUri != null) {
final coordinates = geoUri.path.split(',');
if (coordinates.length == 2) {
final lat = double.tryParse(coordinates[0]);
final lon = double.tryParse(coordinates[1]);
if (lat != null && lon != null) {
double? zoom;
final zoomString = geoUri.queryParameters['z'];
if (zoomString != null) {
zoom = double.tryParse(zoomString);
}
return (LatLng(lat, lon), zoom);
}
}
}
}
return null;
}

View file

@ -3,7 +3,9 @@ import 'dart:math';
import 'package:aves/app_flavor.dart';
import 'package:aves/app_mode.dart';
import 'package:aves/geo/uri.dart';
import 'package:aves/l10n/l10n.dart';
import 'package:aves/model/app/intent.dart';
import 'package:aves/model/app_inventory.dart';
import 'package:aves/model/device.dart';
import 'package:aves/model/filters/recent.dart';
@ -33,6 +35,7 @@ import 'package:aves/widgets/common/providers/durations_provider.dart';
import 'package:aves/widgets/common/providers/highlight_info_provider.dart';
import 'package:aves/widgets/common/providers/media_query_data_provider.dart';
import 'package:aves/widgets/common/providers/viewer_entry_provider.dart';
import 'package:aves/widgets/dialogs/entry_editors/edit_location_dialog.dart';
import 'package:aves/widgets/home_page.dart';
import 'package:aves/widgets/navigation/tv_page_transitions.dart';
import 'package:aves/widgets/navigation/tv_rail.dart';
@ -42,11 +45,13 @@ import 'package:aves_utils/aves_utils.dart';
import 'package:collection/collection.dart';
import 'package:dynamic_color/dynamic_color.dart';
import 'package:equatable/equatable.dart';
import 'package:event_bus/event_bus.dart';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_localization_nn/flutter_localization_nn.dart';
import 'package:intl/intl.dart';
import 'package:latlong2/latlong.dart';
import 'package:overlay_support/overlay_support.dart';
import 'package:provider/provider.dart';
import 'package:screen_brightness/screen_brightness.dart';
@ -88,6 +93,8 @@ class AvesApp extends StatefulWidget {
static ScreenBrightness? get screenBrightness => _AvesAppState._screenBrightness;
static EventBus get intentEventBus => _AvesAppState._intentEventBus;
const AvesApp({
super.key,
required this.flavor,
@ -159,7 +166,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
final MediaStoreSource _mediaStoreSource = MediaStoreSource();
Size? _screenSize;
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder);
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(_defaultPageTransitionsBuilder);
final ValueNotifier<TvMediaQueryModifier?> _tvMediaQueryModifierNotifier = ValueNotifier(null);
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.initialization);
@ -176,10 +183,11 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0)
// - `PredictiveBackPageTransitionsBuilder` for Android 15 / API 35 intra-app predictive back
static const defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
static const _defaultPageTransitionsBuilder = FadeUpwardsPageTransitionsBuilder();
static final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
static ScreenBrightness? _screenBrightness;
static bool _exitedMainByPop = false;
static final EventBus _intentEventBus = EventBus();
@override
void initState() {
@ -534,7 +542,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
await windowService.requestOrientation(Orientation.landscape);
}
} else {
_pageTransitionsBuilderNotifier.value = defaultPageTransitionsBuilder;
_pageTransitionsBuilderNotifier.value = _defaultPageTransitionsBuilder;
_tvMediaQueryModifierNotifier.value = null;
await windowService.requestOrientation(null);
}
@ -627,6 +635,17 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
]);
}
// at this level `ModalRoute.of(context)` is null,
// so we use the global navigator as a workaround
String? getCurrentRouteName() {
String? currentRoute;
_navigatorKey.currentState?.popUntil((route) {
currentRoute = route.settings.name;
return true;
});
return currentRoute;
}
void _onNewIntent(Map? intentData) {
reportService.log('New intent data=$intentData');
@ -641,6 +660,20 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
}
}
if (intentData != null) {
final intentAction = intentData[IntentDataKeys.action] as String?;
if (intentAction == IntentActions.viewGeo) {
final locationZoom = parseGeoUri(intentData[IntentDataKeys.uri] as String?);
if (locationZoom != null && getCurrentRouteName() == EditEntryLocationDialog.routeName) {
// do not push a new route but pass the provided location to the dialog
final location = locationZoom.$1;
debugPrint('Use received location $location for input');
_intentEventBus.fire(LocationReceivedEvent(location));
return;
}
}
}
_navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
settings: const RouteSettings(name: HomePage.routeName),
builder: (_) => _getFirstPage(intentData: intentData),
@ -688,3 +721,9 @@ class AvesScrollBehavior extends MaterialScrollBehavior {
}
typedef TvMediaQueryModifier = MediaQueryData Function(MediaQueryData);
class LocationReceivedEvent {
final LatLng location;
const LocationReceivedEvent(this.location);
}

View file

@ -1,3 +1,5 @@
import 'dart:async';
import 'package:aves/model/entry/entry.dart';
import 'package:aves/model/entry/extensions/location.dart';
import 'package:aves/model/entry/extensions/metadata_edition.dart';
@ -10,6 +12,7 @@ import 'package:aves/theme/durations.dart';
import 'package:aves/theme/icons.dart';
import 'package:aves/theme/themes.dart';
import 'package:aves/view/view.dart';
import 'package:aves/widgets/aves_app.dart';
import 'package:aves/widgets/common/basic/text_dropdown_button.dart';
import 'package:aves/widgets/common/extensions/build_context.dart';
import 'package:aves/widgets/common/fx/transitions.dart';
@ -42,6 +45,7 @@ class EditEntryLocationDialog extends StatefulWidget {
}
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
final List<StreamSubscription> _subscriptions = [];
LocationEditAction _action = LocationEditAction.chooseOnMap;
LatLng? _mapCoordinates;
late AvesEntry _copyItemSource;
@ -56,6 +60,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
_initMapCoordinates();
_initCopyItem();
_initCustom();
AvesApp.intentEventBus.on<LocationReceivedEvent>().listen((event) => _setCustomLocation(event.location));
}
void _initMapCoordinates() {
@ -82,6 +87,9 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
@override
void dispose() {
_subscriptions
..forEach((sub) => sub.cancel())
..clear();
_latitudeController.dispose();
_longitudeController.dispose();
_isValidNotifier.dispose();
@ -151,8 +159,6 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
}
Widget _buildChooseOnMapContent(BuildContext context) {
final l10n = context.l10n;
return Padding(
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
child: Row(
@ -162,13 +168,21 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
IconButton(
icon: const Icon(AIcons.map),
onPressed: _pickLocation,
tooltip: l10n.editEntryLocationDialogChooseOnMap,
tooltip: context.l10n.editEntryLocationDialogChooseOnMap,
),
],
),
);
}
void _setCustomLocation(LatLng latLng) {
_latitudeController.text = coordinateFormatter.format(latLng.latitude);
_longitudeController.text = coordinateFormatter.format(latLng.longitude);
_action = LocationEditAction.setCustom;
_validate();
setState(() {});
}
CollectionLens? _createPickCollection() {
final baseCollection = widget.collection;
return baseCollection != null

View file

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:aves/app_mode.dart';
import 'package:aves/geo/uri.dart';
import 'package:aves/model/app/intent.dart';
import 'package:aves/model/app/permissions.dart';
import 'package:aves/model/app_inventory.dart';
@ -65,7 +66,7 @@ class _HomePageState extends State<HomePage> {
String? _initialRouteName, _initialSearchQuery;
Set<CollectionFilter>? _initialFilters;
String? _initialExplorerPath;
(LatLng, double)? _initialLocationZoom;
(LatLng, double?)? _initialLocationZoom;
List<String>? _secureUris;
static const allowedShortcutRoutes = [
@ -126,26 +127,11 @@ class _HomePageState extends State<HomePage> {
case IntentActions.viewGeo:
error = true;
if (intentUri != null) {
final geoUri = Uri.tryParse(intentUri);
if (geoUri != null) {
// e.g. `geo:44.4361283,26.1027248?z=4.0(Bucharest)`
// cf https://en.wikipedia.org/wiki/Geo_URI_scheme
// cf https://developer.android.com/guide/components/intents-common#ViewMap
final coordinates = geoUri.path.split(',');
if (coordinates.length == 2) {
final lat = double.tryParse(coordinates[0]);
final lon = double.tryParse(coordinates[1]);
if (lat != null && lon != null) {
double? zoom;
final zoomString = geoUri.queryParameters['z'];
if (zoomString != null) {
zoom = double.tryParse(zoomString);
}
_initialRouteName = MapPage.routeName;
_initialLocationZoom = (LatLng(lat, lon), zoom ?? settings.infoMapZoom);
error = false;
}
}
final locationZoom = parseGeoUri(intentUri);
if (locationZoom != null) {
_initialRouteName = MapPage.routeName;
_initialLocationZoom = locationZoom;
error = false;
}
}
break;
@ -228,7 +214,6 @@ class _HomePageState extends State<HomePage> {
context.read<ValueNotifier<AppMode>>().value = appMode;
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms');
switch (appMode) {
case AppMode.main:
@ -273,6 +258,8 @@ class _HomePageState extends State<HomePage> {
break;
}
debugPrint('Home setup complete in ${stopwatch.elapsed.inMilliseconds}ms');
// `pushReplacement` is not enough in some edge cases
// e.g. when opening the viewer in `view` mode should replace a viewer in `main` mode
unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil(

View file

@ -277,6 +277,7 @@ class _InfoPageContentState extends State<_InfoPageContent> {
void _onActionDelegateEvent(ActionEvent<EntryAction> event) {
Future.delayed(ADurations.dialogTransitionLoose).then((_) {
if (!mounted) return;
if (event is ActionStartedEvent) {
_isEditingMetadataNotifier.value = event.action;
} else if (event is ActionEndedEvent) {