#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
|
### 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
|
- 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
|
## <a id="v1.11.13"></a>[v1.11.13] - 2024-09-17
|
||||||
|
|
||||||
### Added
|
### 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_flavor.dart';
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
|
import 'package:aves/geo/uri.dart';
|
||||||
import 'package:aves/l10n/l10n.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/app_inventory.dart';
|
||||||
import 'package:aves/model/device.dart';
|
import 'package:aves/model/device.dart';
|
||||||
import 'package:aves/model/filters/recent.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/highlight_info_provider.dart';
|
||||||
import 'package:aves/widgets/common/providers/media_query_data_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/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/home_page.dart';
|
||||||
import 'package:aves/widgets/navigation/tv_page_transitions.dart';
|
import 'package:aves/widgets/navigation/tv_page_transitions.dart';
|
||||||
import 'package:aves/widgets/navigation/tv_rail.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:collection/collection.dart';
|
||||||
import 'package:dynamic_color/dynamic_color.dart';
|
import 'package:dynamic_color/dynamic_color.dart';
|
||||||
import 'package:equatable/equatable.dart';
|
import 'package:equatable/equatable.dart';
|
||||||
|
import 'package:event_bus/event_bus.dart';
|
||||||
import 'package:flutter/foundation.dart';
|
import 'package:flutter/foundation.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import 'package:flutter_localization_nn/flutter_localization_nn.dart';
|
import 'package:flutter_localization_nn/flutter_localization_nn.dart';
|
||||||
import 'package:intl/intl.dart';
|
import 'package:intl/intl.dart';
|
||||||
|
import 'package:latlong2/latlong.dart';
|
||||||
import 'package:overlay_support/overlay_support.dart';
|
import 'package:overlay_support/overlay_support.dart';
|
||||||
import 'package:provider/provider.dart';
|
import 'package:provider/provider.dart';
|
||||||
import 'package:screen_brightness/screen_brightness.dart';
|
import 'package:screen_brightness/screen_brightness.dart';
|
||||||
|
@ -88,6 +93,8 @@ class AvesApp extends StatefulWidget {
|
||||||
|
|
||||||
static ScreenBrightness? get screenBrightness => _AvesAppState._screenBrightness;
|
static ScreenBrightness? get screenBrightness => _AvesAppState._screenBrightness;
|
||||||
|
|
||||||
|
static EventBus get intentEventBus => _AvesAppState._intentEventBus;
|
||||||
|
|
||||||
const AvesApp({
|
const AvesApp({
|
||||||
super.key,
|
super.key,
|
||||||
required this.flavor,
|
required this.flavor,
|
||||||
|
@ -159,7 +166,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
final MediaStoreSource _mediaStoreSource = MediaStoreSource();
|
final MediaStoreSource _mediaStoreSource = MediaStoreSource();
|
||||||
Size? _screenSize;
|
Size? _screenSize;
|
||||||
|
|
||||||
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(defaultPageTransitionsBuilder);
|
final ValueNotifier<PageTransitionsBuilder> _pageTransitionsBuilderNotifier = ValueNotifier(_defaultPageTransitionsBuilder);
|
||||||
final ValueNotifier<TvMediaQueryModifier?> _tvMediaQueryModifierNotifier = ValueNotifier(null);
|
final ValueNotifier<TvMediaQueryModifier?> _tvMediaQueryModifierNotifier = ValueNotifier(null);
|
||||||
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.initialization);
|
final ValueNotifier<AppMode> _appModeNotifier = ValueNotifier(AppMode.initialization);
|
||||||
|
|
||||||
|
@ -176,10 +183,11 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
|
// - `OpenUpwardsPageTransitionsBuilder` on Pie / API 28
|
||||||
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0)
|
// - `ZoomPageTransitionsBuilder` on Android 10 / API 29 and above (default in Flutter v3.22.0)
|
||||||
// - `PredictiveBackPageTransitionsBuilder` for Android 15 / API 35 intra-app predictive back
|
// - `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 final GlobalKey<NavigatorState> _navigatorKey = GlobalKey(debugLabel: 'app-navigator');
|
||||||
static ScreenBrightness? _screenBrightness;
|
static ScreenBrightness? _screenBrightness;
|
||||||
static bool _exitedMainByPop = false;
|
static bool _exitedMainByPop = false;
|
||||||
|
static final EventBus _intentEventBus = EventBus();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
|
@ -534,7 +542,7 @@ class _AvesAppState extends State<AvesApp> with WidgetsBindingObserver {
|
||||||
await windowService.requestOrientation(Orientation.landscape);
|
await windowService.requestOrientation(Orientation.landscape);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
_pageTransitionsBuilderNotifier.value = defaultPageTransitionsBuilder;
|
_pageTransitionsBuilderNotifier.value = _defaultPageTransitionsBuilder;
|
||||||
_tvMediaQueryModifierNotifier.value = null;
|
_tvMediaQueryModifierNotifier.value = null;
|
||||||
await windowService.requestOrientation(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) {
|
void _onNewIntent(Map? intentData) {
|
||||||
reportService.log('New intent data=$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(
|
_navigatorKey.currentState!.pushReplacement(DirectMaterialPageRoute(
|
||||||
settings: const RouteSettings(name: HomePage.routeName),
|
settings: const RouteSettings(name: HomePage.routeName),
|
||||||
builder: (_) => _getFirstPage(intentData: intentData),
|
builder: (_) => _getFirstPage(intentData: intentData),
|
||||||
|
@ -688,3 +721,9 @@ class AvesScrollBehavior extends MaterialScrollBehavior {
|
||||||
}
|
}
|
||||||
|
|
||||||
typedef TvMediaQueryModifier = MediaQueryData Function(MediaQueryData);
|
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/entry.dart';
|
||||||
import 'package:aves/model/entry/extensions/location.dart';
|
import 'package:aves/model/entry/extensions/location.dart';
|
||||||
import 'package:aves/model/entry/extensions/metadata_edition.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/icons.dart';
|
||||||
import 'package:aves/theme/themes.dart';
|
import 'package:aves/theme/themes.dart';
|
||||||
import 'package:aves/view/view.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/basic/text_dropdown_button.dart';
|
||||||
import 'package:aves/widgets/common/extensions/build_context.dart';
|
import 'package:aves/widgets/common/extensions/build_context.dart';
|
||||||
import 'package:aves/widgets/common/fx/transitions.dart';
|
import 'package:aves/widgets/common/fx/transitions.dart';
|
||||||
|
@ -42,6 +45,7 @@ class EditEntryLocationDialog extends StatefulWidget {
|
||||||
}
|
}
|
||||||
|
|
||||||
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
|
final List<StreamSubscription> _subscriptions = [];
|
||||||
LocationEditAction _action = LocationEditAction.chooseOnMap;
|
LocationEditAction _action = LocationEditAction.chooseOnMap;
|
||||||
LatLng? _mapCoordinates;
|
LatLng? _mapCoordinates;
|
||||||
late AvesEntry _copyItemSource;
|
late AvesEntry _copyItemSource;
|
||||||
|
@ -56,6 +60,7 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
_initMapCoordinates();
|
_initMapCoordinates();
|
||||||
_initCopyItem();
|
_initCopyItem();
|
||||||
_initCustom();
|
_initCustom();
|
||||||
|
AvesApp.intentEventBus.on<LocationReceivedEvent>().listen((event) => _setCustomLocation(event.location));
|
||||||
}
|
}
|
||||||
|
|
||||||
void _initMapCoordinates() {
|
void _initMapCoordinates() {
|
||||||
|
@ -82,6 +87,9 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
|
_subscriptions
|
||||||
|
..forEach((sub) => sub.cancel())
|
||||||
|
..clear();
|
||||||
_latitudeController.dispose();
|
_latitudeController.dispose();
|
||||||
_longitudeController.dispose();
|
_longitudeController.dispose();
|
||||||
_isValidNotifier.dispose();
|
_isValidNotifier.dispose();
|
||||||
|
@ -151,8 +159,6 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildChooseOnMapContent(BuildContext context) {
|
Widget _buildChooseOnMapContent(BuildContext context) {
|
||||||
final l10n = context.l10n;
|
|
||||||
|
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
padding: const EdgeInsetsDirectional.only(start: 16, end: 8),
|
||||||
child: Row(
|
child: Row(
|
||||||
|
@ -162,13 +168,21 @@ class _EditEntryLocationDialogState extends State<EditEntryLocationDialog> {
|
||||||
IconButton(
|
IconButton(
|
||||||
icon: const Icon(AIcons.map),
|
icon: const Icon(AIcons.map),
|
||||||
onPressed: _pickLocation,
|
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() {
|
CollectionLens? _createPickCollection() {
|
||||||
final baseCollection = widget.collection;
|
final baseCollection = widget.collection;
|
||||||
return baseCollection != null
|
return baseCollection != null
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
import 'dart:async';
|
import 'dart:async';
|
||||||
|
|
||||||
import 'package:aves/app_mode.dart';
|
import 'package:aves/app_mode.dart';
|
||||||
|
import 'package:aves/geo/uri.dart';
|
||||||
import 'package:aves/model/app/intent.dart';
|
import 'package:aves/model/app/intent.dart';
|
||||||
import 'package:aves/model/app/permissions.dart';
|
import 'package:aves/model/app/permissions.dart';
|
||||||
import 'package:aves/model/app_inventory.dart';
|
import 'package:aves/model/app_inventory.dart';
|
||||||
|
@ -65,7 +66,7 @@ class _HomePageState extends State<HomePage> {
|
||||||
String? _initialRouteName, _initialSearchQuery;
|
String? _initialRouteName, _initialSearchQuery;
|
||||||
Set<CollectionFilter>? _initialFilters;
|
Set<CollectionFilter>? _initialFilters;
|
||||||
String? _initialExplorerPath;
|
String? _initialExplorerPath;
|
||||||
(LatLng, double)? _initialLocationZoom;
|
(LatLng, double?)? _initialLocationZoom;
|
||||||
List<String>? _secureUris;
|
List<String>? _secureUris;
|
||||||
|
|
||||||
static const allowedShortcutRoutes = [
|
static const allowedShortcutRoutes = [
|
||||||
|
@ -126,26 +127,11 @@ class _HomePageState extends State<HomePage> {
|
||||||
case IntentActions.viewGeo:
|
case IntentActions.viewGeo:
|
||||||
error = true;
|
error = true;
|
||||||
if (intentUri != null) {
|
if (intentUri != null) {
|
||||||
final geoUri = Uri.tryParse(intentUri);
|
final locationZoom = parseGeoUri(intentUri);
|
||||||
if (geoUri != null) {
|
if (locationZoom != null) {
|
||||||
// e.g. `geo:44.4361283,26.1027248?z=4.0(Bucharest)`
|
_initialRouteName = MapPage.routeName;
|
||||||
// cf https://en.wikipedia.org/wiki/Geo_URI_scheme
|
_initialLocationZoom = locationZoom;
|
||||||
// cf https://developer.android.com/guide/components/intents-common#ViewMap
|
error = false;
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
|
@ -228,7 +214,6 @@ class _HomePageState extends State<HomePage> {
|
||||||
|
|
||||||
context.read<ValueNotifier<AppMode>>().value = appMode;
|
context.read<ValueNotifier<AppMode>>().value = appMode;
|
||||||
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
|
unawaited(reportService.setCustomKey('app_mode', appMode.toString()));
|
||||||
debugPrint('Storage check complete in ${stopwatch.elapsed.inMilliseconds}ms');
|
|
||||||
|
|
||||||
switch (appMode) {
|
switch (appMode) {
|
||||||
case AppMode.main:
|
case AppMode.main:
|
||||||
|
@ -273,6 +258,8 @@ class _HomePageState extends State<HomePage> {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
debugPrint('Home setup complete in ${stopwatch.elapsed.inMilliseconds}ms');
|
||||||
|
|
||||||
// `pushReplacement` is not enough in some edge cases
|
// `pushReplacement` is not enough in some edge cases
|
||||||
// e.g. when opening the viewer in `view` mode should replace a viewer in `main` mode
|
// e.g. when opening the viewer in `view` mode should replace a viewer in `main` mode
|
||||||
unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
unawaited(Navigator.maybeOf(context)?.pushAndRemoveUntil(
|
||||||
|
|
|
@ -277,6 +277,7 @@ class _InfoPageContentState extends State<_InfoPageContent> {
|
||||||
|
|
||||||
void _onActionDelegateEvent(ActionEvent<EntryAction> event) {
|
void _onActionDelegateEvent(ActionEvent<EntryAction> event) {
|
||||||
Future.delayed(ADurations.dialogTransitionLoose).then((_) {
|
Future.delayed(ADurations.dialogTransitionLoose).then((_) {
|
||||||
|
if (!mounted) return;
|
||||||
if (event is ActionStartedEvent) {
|
if (event is ActionStartedEvent) {
|
||||||
_isEditingMetadataNotifier.value = event.action;
|
_isEditingMetadataNotifier.value = event.action;
|
||||||
} else if (event is ActionEndedEvent) {
|
} else if (event is ActionEndedEvent) {
|
||||||
|
|
Loading…
Reference in a new issue