#1087 open external map app from map views

This commit is contained in:
Thibault Deckers 2024-08-04 19:41:25 +02:00
parent 2d5a7f6c27
commit 4d80dfe1d6
8 changed files with 109 additions and 56 deletions

View file

@ -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 - Viewer: display more items in tag/copy/move quick action choosers
- Collection: sort by duration - Collection: sort by duration
- Map: open external map app from map views
### Changed ### Changed

View file

@ -8,6 +8,7 @@ extension ExtraMapActionView on MapAction {
final l10n = context.l10n; final l10n = context.l10n;
return switch (this) { return switch (this) {
MapAction.selectStyle => l10n.mapStyleTooltip, MapAction.selectStyle => l10n.mapStyleTooltip,
MapAction.openMapApp => l10n.entryActionOpenMap,
MapAction.zoomIn => l10n.mapZoomInTooltip, MapAction.zoomIn => l10n.mapZoomInTooltip,
MapAction.zoomOut => l10n.mapZoomOutTooltip, MapAction.zoomOut => l10n.mapZoomOutTooltip,
}; };
@ -18,6 +19,7 @@ extension ExtraMapActionView on MapAction {
IconData _getIconData() { IconData _getIconData() {
return switch (this) { return switch (this) {
MapAction.selectStyle => AIcons.layers, MapAction.selectStyle => AIcons.layers,
MapAction.openMapApp => AIcons.openOutside,
MapAction.zoomIn => AIcons.zoomIn, MapAction.zoomIn => AIcons.zoomIn,
MapAction.zoomOut => AIcons.zoomOut, MapAction.zoomOut => AIcons.zoomOut,
}; };

View file

@ -124,7 +124,13 @@ class MapButtonPanel extends StatelessWidget {
Padding( Padding(
padding: EdgeInsets.only(top: padding), padding: EdgeInsets.only(top: padding),
// key is expected by test driver // 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),
],
),
), ),
], ],
), ),

View file

@ -26,6 +26,8 @@ class MapActionDelegate {
), ),
onSelection: (v) => settings.mapStyle = v, onSelection: (v) => settings.mapStyle = v,
); );
case MapAction.openMapApp:
OpenMapAppNotification().dispatch(context);
case MapAction.zoomIn: case MapAction.zoomIn:
controller?.zoomBy(1); controller?.zoomBy(1);
case MapAction.zoomOut: case MapAction.zoomOut:
@ -33,3 +35,5 @@ class MapActionDelegate {
} }
} }
} }
class OpenMapAppNotification extends Notification {}

View file

@ -12,6 +12,7 @@ import 'package:aves/model/settings/enums/map_style.dart';
import 'package:aves/model/settings/settings.dart'; import 'package:aves/model/settings/settings.dart';
import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_lens.dart';
import 'package:aves/model/source/tag.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/durations.dart';
import 'package:aves/theme/icons.dart'; import 'package:aves/theme/icons.dart';
import 'package:aves/view/view.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/map/map_action_delegate.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/map_theme_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/filter_grids/common/action_delegates/chip.dart';
import 'package:aves/widgets/map/scroller.dart'; import 'package:aves/widgets/map/scroller.dart';
import 'package:aves/widgets/viewer/controls/notifications.dart'; import 'package:aves/widgets/viewer/controls/notifications.dart';
@ -188,6 +190,8 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
_goToCollection(notification.filter); _goToCollection(notification.filter);
} else if (notification is FilterNotification) { } else if (notification is FilterNotification) {
_goToCollection(notification.filter); _goToCollection(notification.filter);
} else if (notification is OpenMapAppNotification) {
_openMapApp();
} else { } else {
return false; return false;
} }
@ -434,6 +438,15 @@ class _ContentState extends State<_Content> with SingleTickerProviderStateMixin
); );
} }
Future<void> _openMapApp() async {
final latLng = _dotEntryNotifier.value?.latLng ?? _mapController.idleBounds?.projectedCenter;
if (latLng != null) {
await appService.openMap(latLng).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
}
}
// overlay // overlay
void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value; void _toggleOverlay() => _overlayVisible.value = !_overlayVisible.value;

View file

@ -10,7 +10,9 @@ import 'package:aves/theme/icons.dart';
import 'package:aves/widgets/common/extensions/build_context.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/identity/aves_filter_chip.dart';
import 'package:aves/widgets/common/map/geo_map.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/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/map/map_page.dart';
import 'package:aves/widgets/viewer/info/common.dart'; import 'package:aves/widgets/viewer/info/common.dart';
import 'package:aves_map/aves_map.dart'; import 'package:aves_map/aves_map.dart';
@ -76,63 +78,72 @@ class _LocationSectionState extends State<LocationSection> {
if (!entry.hasGps) return const SizedBox(); if (!entry.hasGps) return const SizedBox();
final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate); final canNavigate = context.select<ValueNotifier<AppMode>, bool>((v) => v.value.canNavigate);
return Column( return NotificationListener(
crossAxisAlignment: CrossAxisAlignment.start, onNotification: (notification) {
children: [ if (notification is OpenMapAppNotification) {
if (widget.showTitle) const SectionRow(icon: AIcons.location), _openMapApp();
MapTheme( return true;
interactive: false, }
showCoordinateFilter: false, return false;
navigationButton: canNavigate ? MapNavigationButton.map : MapNavigationButton.none, },
visualDensity: VisualDensity.compact, child: Column(
mapHeight: 200, crossAxisAlignment: CrossAxisAlignment.start,
child: GeoMap( children: [
controller: _mapController, if (widget.showTitle) const SectionRow(icon: AIcons.location),
entries: [entry], MapTheme(
availableSize: MediaQuery.sizeOf(context), interactive: false,
isAnimatingNotifier: widget.isScrollingNotifier, showCoordinateFilter: false,
onUserZoomChange: (zoom) => settings.infoMapZoom = zoom.roundToDouble(), navigationButton: canNavigate ? MapNavigationButton.map : MapNavigationButton.none,
onMarkerTap: collection != null && canNavigate ? (location, entry) => _openMapPage(context) : null, visualDensity: VisualDensity.compact,
openMapPage: collection != null ? _openMapPage : null, 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(
AnimatedBuilder( animation: entry.addressChangeNotifier,
animation: entry.addressChangeNotifier, builder: (context, child) {
builder: (context, child) { final filters = <LocationFilter>[];
final filters = <LocationFilter>[]; if (entry.hasAddress) {
if (entry.hasAddress) { final address = entry.addressDetails!;
final address = entry.addressDetails!; final country = address.countryName;
final country = address.countryName; if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}'));
if (country != null && country.isNotEmpty) filters.add(LocationFilter(LocationLevel.country, '$country${LocationFilter.locationSeparator}${address.countryCode}')); final state = address.stateName;
final state = address.stateName; if (state != null && state.isNotEmpty) filters.add(LocationFilter(LocationLevel.state, '$state${LocationFilter.locationSeparator}${address.stateCode}'));
if (state != null && state.isNotEmpty) filters.add(LocationFilter(LocationLevel.state, '$state${LocationFilter.locationSeparator}${address.stateCode}')); final place = address.place;
final place = address.place; if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place));
if (place != null && place.isNotEmpty) filters.add(LocationFilter(LocationLevel.place, place)); }
}
return Column( return Column(
crossAxisAlignment: CrossAxisAlignment.start, crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
_AddressInfoGroup(entry: entry), _AddressInfoGroup(entry: entry),
if (filters.isNotEmpty) if (filters.isNotEmpty)
Padding( Padding(
padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8), padding: const EdgeInsets.symmetric(horizontal: AvesFilterChip.outlineWidth / 2) + const EdgeInsets.only(top: 8),
child: Wrap( child: Wrap(
spacing: 8, spacing: 8,
runSpacing: 8, runSpacing: 8,
children: filters children: filters
.map((filter) => AvesFilterChip( .map((filter) => AvesFilterChip(
filter: filter, filter: filter,
onTap: widget.onFilter, onTap: widget.onFilter,
)) ))
.toList(), .toList(),
),
), ),
), ],
], );
); },
}, ),
), ],
], ),
); );
} }
@ -155,6 +166,15 @@ class _LocationSectionState extends State<LocationSection> {
); );
} }
Future<void> _openMapApp() async {
final latLng = entry.latLng;
if (latLng != null) {
await appService.openMap(latLng).then((success) {
if (!success) showNoMatchingAppDialog(context);
});
}
}
void _onMetadataChanged() { void _onMetadataChanged() {
setState(() {}); setState(() {});

View file

@ -6,6 +6,9 @@ import 'package:latlong2/latlong.dart';
class AvesMapController { class AvesMapController {
final StreamController _streamController = StreamController.broadcast(); final StreamController _streamController = StreamController.broadcast();
ZoomedBounds? _idleBounds;
ZoomedBounds? get idleBounds => _idleBounds;
Stream<dynamic> get _events => _streamController.stream; Stream<dynamic> get _events => _streamController.stream;
@ -38,7 +41,10 @@ class AvesMapController {
void zoomBy(double delta) => _streamController.add(MapControllerZoomEvent(delta)); 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()); void notifyMarkerLocationChange() => _streamController.add(MapMarkerLocationChangeEvent());
} }

View file

@ -1,5 +1,6 @@
enum MapAction { enum MapAction {
selectStyle, selectStyle,
openMapApp,
zoomIn, zoomIn,
zoomOut, zoomOut,
} }