#1087 open external map app from map views
This commit is contained in:
parent
2d5a7f6c27
commit
4d80dfe1d6
8 changed files with 109 additions and 56 deletions
|
@ -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
|
||||||
|
|
||||||
|
|
|
@ -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,
|
||||||
};
|
};
|
||||||
|
|
|
@ -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),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
|
@ -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 {}
|
||||||
|
|
|
@ -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;
|
||||||
|
|
|
@ -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(() {});
|
||||||
|
|
||||||
|
|
|
@ -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());
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
enum MapAction {
|
enum MapAction {
|
||||||
selectStyle,
|
selectStyle,
|
||||||
|
openMapApp,
|
||||||
zoomIn,
|
zoomIn,
|
||||||
zoomOut,
|
zoomOut,
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in a new issue