#1211 receiving geo: uri while editing location fills in coordinates
This commit is contained in:
parent
ec59e348c5
commit
d859887319
6 changed files with 104 additions and 29 deletions
10
CHANGELOG.md
10
CHANGELOG.md
|
@ -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
26
lib/geo/uri.dart
Normal 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;
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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(
|
||||
|
|
|
@ -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) {
|
||||
|
|
Loading…
Reference in a new issue