diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d82bc55a..660ad0c5b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ All notable changes to this project will be documented in this file. - Viewer: display more items in tag/copy/move quick action choosers - Collection: sort by duration +- Map: open external map app from map views ### Changed diff --git a/lib/view/src/actions/map.dart b/lib/view/src/actions/map.dart index ee7db32fc..c4d22a684 100644 --- a/lib/view/src/actions/map.dart +++ b/lib/view/src/actions/map.dart @@ -8,6 +8,7 @@ extension ExtraMapActionView on MapAction { final l10n = context.l10n; return switch (this) { MapAction.selectStyle => l10n.mapStyleTooltip, + MapAction.openMapApp => l10n.entryActionOpenMap, MapAction.zoomIn => l10n.mapZoomInTooltip, MapAction.zoomOut => l10n.mapZoomOutTooltip, }; @@ -18,6 +19,7 @@ extension ExtraMapActionView on MapAction { IconData _getIconData() { return switch (this) { MapAction.selectStyle => AIcons.layers, + MapAction.openMapApp => AIcons.openOutside, MapAction.zoomIn => AIcons.zoomIn, MapAction.zoomOut => AIcons.zoomOut, }; diff --git a/lib/widgets/common/map/buttons/panel.dart b/lib/widgets/common/map/buttons/panel.dart index c25427800..f329bbb6d 100644 --- a/lib/widgets/common/map/buttons/panel.dart +++ b/lib/widgets/common/map/buttons/panel.dart @@ -124,7 +124,13 @@ class MapButtonPanel extends StatelessWidget { Padding( padding: EdgeInsets.only(top: padding), // key is expected by test driver - child: _buildButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')), + child: Column( + children: [ + _buildButton(context, MapAction.selectStyle, buttonKey: const Key('map-menu-layers')), + SizedBox(height: padding), + _buildButton(context, MapAction.openMapApp), + ], + ), ), ], ), diff --git a/lib/widgets/common/map/map_action_delegate.dart b/lib/widgets/common/map/map_action_delegate.dart index d5fe1b079..5c3728e05 100644 --- a/lib/widgets/common/map/map_action_delegate.dart +++ b/lib/widgets/common/map/map_action_delegate.dart @@ -26,6 +26,8 @@ class MapActionDelegate { ), onSelection: (v) => settings.mapStyle = v, ); + case MapAction.openMapApp: + OpenMapAppNotification().dispatch(context); case MapAction.zoomIn: controller?.zoomBy(1); case MapAction.zoomOut: @@ -33,3 +35,5 @@ class MapActionDelegate { } } } + +class OpenMapAppNotification extends Notification {} diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index 4b45ff8cd..30796f1bf 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -12,6 +12,7 @@ import 'package:aves/model/settings/enums/map_style.dart'; import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/tag.dart'; +import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; import 'package:aves/view/view.dart'; @@ -28,6 +29,7 @@ import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/map_action_delegate.dart'; import 'package:aves/widgets/common/providers/highlight_info_provider.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/filter_grids/common/action_delegates/chip.dart'; import 'package:aves/widgets/map/scroller.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart'; @@ -188,6 +190,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin _goToCollection(notification.filter); } else if (notification is FilterNotification) { _goToCollection(notification.filter); + } else if (notification is OpenMapAppNotification) { + _openMapApp(); } else { return false; } @@ -434,6 +438,15 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin ); } + Future _openMapApp() async { + final latLng = _dotEntryNotifier.value?.latLng ?? _mapController.idleBounds?.projectedCenter; + if (latLng != null) { + await appService.openMap(latLng).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); + } + } + // overlay void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value; diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 84606cb3b..536c705e6 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -10,7 +10,9 @@ import 'package:aves/theme/icons.dart'; import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/identity/aves_filter_chip.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; +import 'package:aves/widgets/common/map/map_action_delegate.dart'; import 'package:aves/widgets/common/providers/map_theme_provider.dart'; +import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves_map/aves_map.dart'; @@ -76,63 +78,72 @@ class _LocationSectionState extends State { if (!entry.hasGps) return const SizedBox(); final canNavigate = context.select, bool>((v) => v.value.canNavigate); - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - if (widget.showTitle) const SectionRow(icon: AIcons.location), - MapTheme( - interactive: false, - showCoordinateFilter: false, - navigationButton: canNavigate ? MapNavigationButton.map : MapNavigationButton.none, - visualDensity: VisualDensity.compact, - mapHeight: 200, - child: GeoMap( - controller: _mapController, - entries: [entry], - availableSize: MediaQuery.sizeOf(context), - isAnimatingNotifier: widget.isScrollingNotifier, - onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(), - onMarkerTap: collection != null && canNavigate ? (location, entry) => _openMapPage(context) : null, - openMapPage: collection != null ? _openMapPage : null, + return NotificationListener( + onNotification: (notification) { + if (notification is OpenMapAppNotification) { + _openMapApp(); + return true; + } + return false; + }, + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + if (widget.showTitle) const SectionRow(icon: AIcons.location), + MapTheme( + interactive: false, + showCoordinateFilter: false, + navigationButton: canNavigate ? MapNavigationButton.map : MapNavigationButton.none, + visualDensity: VisualDensity.compact, + mapHeight: 200, + child: GeoMap( + controller: _mapController, + entries: [entry], + availableSize: MediaQuery.sizeOf(context), + isAnimatingNotifier: widget.isScrollingNotifier, + onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(), + onMarkerTap: collection != null && canNavigate ? (location, entry) => _openMapPage(context) : null, + openMapPage: collection != null ? _openMapPage : null, + ), ), - ), - AnimatedBuilder( - animation: entry.addressChangeNotifier, - builder: (context, child) { - final filters = []; - if (entry.hasAddress) { - final address = entry.addressDetails!; - final country = address.countryName; - if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); - final state = address.stateName; - if (state != null && state.isNotEmpty) filters.add(LocationFilter(LocationLevel.state, '$state${LocationFilter.locationSeparator}${address.stateCode}')); - final place = address.place; - if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place)); - } + AnimatedBuilder( + animation: entry.addressChangeNotifier, + builder: (context, child) { + final filters = []; + if (entry.hasAddress) { + final address = entry.addressDetails!; + final country = address.countryName; + if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); + final state = address.stateName; + if (state != null && state.isNotEmpty) filters.add(LocationFilter(LocationLevel.state, '$state${LocationFilter.locationSeparator}${address.stateCode}')); + final place = address.place; + if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place)); + } - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - _AddressInfoGroup(entry: entry), - if (filters.isNotEmpty) - Padding( - padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8), - child: Wrap( - spacing: 8, - runSpacing: 8, - children: filters - .map((filter) => AvesFilterChip( - filter: filter, - onTap: widget.onFilter, - )) - .toList(), + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + _AddressInfoGroup(entry: entry), + if (filters.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8), + child: Wrap( + spacing: 8, + runSpacing: 8, + children: filters + .map((filter) => AvesFilterChip( + filter: filter, + onTap: widget.onFilter, + )) + .toList(), + ), ), - ), - ], - ); - }, - ), - ], + ], + ); + }, + ), + ], + ), ); } @@ -155,6 +166,15 @@ class _LocationSectionState extends State { ); } + Future _openMapApp() async { + final latLng = entry.latLng; + if (latLng != null) { + await appService.openMap(latLng).then((success) { + if (!success) showNoMatchingAppDialog(context); + }); + } + } + void _onMetadataChanged() { setState(() {}); diff --git a/plugins/aves_map/lib/src/controller.dart b/plugins/aves_map/lib/src/controller.dart index 59f7af250..26d2b56c4 100644 --- a/plugins/aves_map/lib/src/controller.dart +++ b/plugins/aves_map/lib/src/controller.dart @@ -6,6 +6,9 @@ import 'package:latlong2/latlong.dart'; class AvesMapController { final StreamController _streamController = StreamController.broadcast(); + ZoomedBounds? _idleBounds; + + ZoomedBounds? get idleBounds => _idleBounds; Stream get _events => _streamController.stream; @@ -38,7 +41,10 @@ class AvesMapController { void zoomBy(double delta) => _streamController.add(MapControllerZoomEvent(delta)); - void notifyIdle(ZoomedBounds bounds) => _streamController.add(MapIdleUpdate(bounds)); + void notifyIdle(ZoomedBounds bounds) { + _idleBounds = bounds; + _streamController.add(MapIdleUpdate(bounds)); + } void notifyMarkerLocationChange() => _streamController.add(MapMarkerLocationChangeEvent()); } diff --git a/plugins/aves_model/lib/src/actions/map.dart b/plugins/aves_model/lib/src/actions/map.dart index f9be8b8d0..6d4838cf3 100644 --- a/plugins/aves_model/lib/src/actions/map.dart +++ b/plugins/aves_model/lib/src/actions/map.dart @@ -1,5 +1,6 @@ enum MapAction { selectStyle, + openMapApp, zoomIn, zoomOut, }