diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index dbe9db7ff..3491ee69d 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -869,6 +869,8 @@ "@mapAttributionOsmHot": {}, "mapAttributionStamen": "Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors • Tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)", "@mapAttributionStamen": {}, + "openMapTooltip": "View on Map page", + "@openMapTooltip": {}, "viewerInfoOpenEmbeddedFailureFeedback": "Failed to extract embedded data", "@viewerInfoOpenEmbeddedFailureFeedback": {}, diff --git a/lib/l10n/app_ko.arb b/lib/l10n/app_ko.arb index 6ad654cc4..8221a6519 100644 --- a/lib/l10n/app_ko.arb +++ b/lib/l10n/app_ko.arb @@ -424,6 +424,7 @@ "mapPointNorthUpTooltip": "북쪽을 위로 가리키기", "mapAttributionOsmHot": "지도 데이터 © [OpenStreetMap](https://www.openstreetmap.org/copyright) 기여자 • 타일 [HOT](https://www.hotosm.org/) • 호스팅 [OSM France](https://openstreetmap.fr/)", "mapAttributionStamen": "지도 데이터 © [OpenStreetMap](https://www.openstreetmap.org/copyright) 기여자 • 타일 [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0)", + "openMapTooltip": "지도 페이지에서 보기", "viewerInfoOpenEmbeddedFailureFeedback": "첨부 데이터 추출 오류", "viewerInfoOpenLinkText": "열기", diff --git a/lib/widgets/common/map/buttons.dart b/lib/widgets/common/map/buttons.dart index 5112810a5..0045a63d0 100644 --- a/lib/widgets/common/map/buttons.dart +++ b/lib/widgets/common/map/buttons.dart @@ -1,7 +1,7 @@ import 'package:aves/model/settings/enums.dart'; import 'package:aves/model/settings/map_style.dart'; import 'package:aves/model/settings/settings.dart'; -import 'package:aves/services/android_app_service.dart'; +import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/services/common/services.dart'; import 'package:aves/theme/durations.dart'; import 'package:aves/theme/icons.dart'; @@ -9,16 +9,17 @@ import 'package:aves/widgets/common/extensions/build_context.dart'; import 'package:aves/widgets/common/fx/blurred.dart'; import 'package:aves/widgets/common/fx/borders.dart'; import 'package:aves/widgets/common/map/compass.dart'; +import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves/widgets/common/map/zoomed_bounds.dart'; -import 'package:aves/widgets/dialogs/aves_dialog.dart'; import 'package:aves/widgets/dialogs/aves_selection_dialog.dart'; +import 'package:aves/widgets/map/map_page.dart'; import 'package:aves/widgets/viewer/overlay/common.dart'; import 'package:flutter/material.dart'; import 'package:flutter/scheduler.dart'; import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; class MapButtonPanel extends StatelessWidget { - final bool showBackButton; final ValueNotifier boundsNotifier; final Future Function(double amount)? zoomBy; final VoidCallback? resetRotation; @@ -27,7 +28,6 @@ class MapButtonPanel extends StatelessWidget { const MapButtonPanel({ Key? key, - required this.showBackButton, required this.boundsNotifier, this.zoomBy, this.resetRotation, @@ -37,6 +37,28 @@ class MapButtonPanel extends StatelessWidget { Widget build(BuildContext context) { final iconTheme = IconTheme.of(context); final iconSize = Size.square(iconTheme.size!); + + Widget? navigationButton; + switch (context.select((v) => v.navigationButton)) { + case MapNavigationButton.back: + navigationButton = MapOverlayButton( + icon: const BackButtonIcon(), + onPressed: () => Navigator.pop(context), + tooltip: MaterialLocalizations.of(context).backButtonTooltip, + ); + break; + case MapNavigationButton.map: + final collection = context.read(); + if (collection != null) { + navigationButton = MapOverlayButton( + icon: const Icon(AIcons.map), + onPressed: () => _goToMap(context, collection), + tooltip: context.l10n.openMapTooltip, + ); + } + break; + } + return Positioned.fill( child: Align( alignment: AlignmentDirectional.centerEnd, @@ -53,43 +75,38 @@ class MapButtonPanel extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (showBackButton) - MapOverlayButton( - icon: const BackButtonIcon(), - onPressed: () => Navigator.pop(context), - tooltip: MaterialLocalizations.of(context).backButtonTooltip, - ), - if (resetRotation != null) ...[ + if (navigationButton != null) ...[ + navigationButton, const SizedBox(height: padding), - ValueListenableBuilder( - valueListenable: boundsNotifier, - builder: (context, bounds, child) { - final degrees = bounds.rotation; - final opacity = degrees == 0 ? .0 : 1.0; - return IgnorePointer( - ignoring: opacity == 0, - child: AnimatedOpacity( - opacity: opacity, - duration: Durations.viewerOverlayAnimation, - child: MapOverlayButton( - icon: Transform( - origin: iconSize.center(Offset.zero), - transform: Matrix4.rotationZ(degToRadian(degrees)), - child: CustomPaint( - painter: CompassPainter( - color: iconTheme.color!, - ), - size: iconSize, - ), - ), - onPressed: () => resetRotation?.call(), - tooltip: context.l10n.mapPointNorthUpTooltip, - ), - ), - ); - }, - ), ], + ValueListenableBuilder( + valueListenable: boundsNotifier, + builder: (context, bounds, child) { + final degrees = bounds.rotation; + final opacity = degrees == 0 ? .0 : 1.0; + return IgnorePointer( + ignoring: opacity == 0, + child: AnimatedOpacity( + opacity: opacity, + duration: Durations.viewerOverlayAnimation, + child: MapOverlayButton( + icon: Transform( + origin: iconSize.center(Offset.zero), + transform: Matrix4.rotationZ(degToRadian(degrees)), + child: CustomPaint( + painter: CompassPainter( + color: iconTheme.color!, + ), + size: iconSize, + ), + ), + onPressed: () => resetRotation?.call(), + tooltip: context.l10n.mapPointNorthUpTooltip, + ), + ), + ); + }, + ), ], ), ), @@ -98,14 +115,6 @@ class MapButtonPanel extends StatelessWidget { child: Column( mainAxisSize: MainAxisSize.min, children: [ - MapOverlayButton( - icon: const Icon(AIcons.openOutside), - onPressed: () => AndroidAppService.openMap(boundsNotifier.value.center).then((success) { - if (!success) showNoMatchingAppDialog(context); - }), - tooltip: context.l10n.entryActionOpenMap, - ), - const SizedBox(height: padding), MapOverlayButton( icon: const Icon(AIcons.layers), onPressed: () async { @@ -161,6 +170,20 @@ class MapButtonPanel extends StatelessWidget { ), ); } + + void _goToMap(BuildContext context, CollectionLens collection) { + final entries = collection.sortedEntries.where((entry) => entry.hasGps).toList(); + + Navigator.push( + context, + MaterialPageRoute( + settings: const RouteSettings(name: MapPage.routeName), + builder: (context) => MapPage( + entries: entries, + ), + ), + ); + } } class MapOverlayButton extends StatelessWidget { @@ -177,6 +200,7 @@ class MapOverlayButton extends StatelessWidget { @override Widget build(BuildContext context) { + final visualDensity = context.select((v) => v.visualDensity); final blurred = settings.enableOverlayBlurEffect; return BlurredOval( enabled: blurred, @@ -190,7 +214,7 @@ class MapOverlayButton extends StatelessWidget { ), child: IconButton( iconSize: 20, - visualDensity: VisualDensity.compact, + visualDensity: visualDensity, icon: icon, onPressed: onPressed, tooltip: tooltip, diff --git a/lib/widgets/common/map/decorator.dart b/lib/widgets/common/map/decorator.dart index 58aedd0e2..d3f6dc7b8 100644 --- a/lib/widgets/common/map/decorator.dart +++ b/lib/widgets/common/map/decorator.dart @@ -1,7 +1,8 @@ +import 'package:aves/widgets/common/map/theme.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class MapDecorator extends StatelessWidget { - final bool interactive; final Widget? child; static const mapBorderRadius = BorderRadius.all(Radius.circular(24)); // to match button circles @@ -10,12 +11,12 @@ class MapDecorator extends StatelessWidget { const MapDecorator({ Key? key, - required this.interactive, this.child, }) : super(key: key); @override Widget build(BuildContext context) { + final interactive = context.select((v) => v.interactive); return GestureDetector( onScaleStart: interactive ? null diff --git a/lib/widgets/common/map/geo_map.dart b/lib/widgets/common/map/geo_map.dart index b6f7efd5e..bbbf01a3e 100644 --- a/lib/widgets/common/map/geo_map.dart +++ b/lib/widgets/common/map/geo_map.dart @@ -16,6 +16,7 @@ import 'package:aves/widgets/common/map/geo_entry.dart'; import 'package:aves/widgets/common/map/google/map.dart'; import 'package:aves/widgets/common/map/leaflet/map.dart'; import 'package:aves/widgets/common/map/marker.dart'; +import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:collection/collection.dart'; import 'package:equatable/equatable.dart'; @@ -27,8 +28,6 @@ import 'package:provider/provider.dart'; class GeoMap extends StatefulWidget { final AvesMapController? controller; final List entries; - final bool interactive, showBackButton; - final double? mapHeight; final ValueNotifier isAnimatingNotifier; final UserZoomChangeCallback? onUserZoomChange; final MarkerTapCallback? onMarkerTap; @@ -40,9 +39,6 @@ class GeoMap extends StatefulWidget { Key? key, this.controller, required this.entries, - required this.interactive, - required this.showBackButton, - this.mapHeight, required this.isAnimatingNotifier, this.onUserZoomChange, this.onMarkerTap, @@ -64,12 +60,6 @@ class _GeoMapState extends State { List get entries => widget.entries; - bool get interactive => widget.interactive; - - bool get showBackButton => widget.showBackButton; - - double? get mapHeight => widget.mapHeight; - @override void initState() { super.initState(); @@ -139,8 +129,6 @@ class _GeoMapState extends State { ? EntryGoogleMap( controller: widget.controller, boundsNotifier: _boundsNotifier, - interactive: interactive, - showBackButton: showBackButton, minZoom: 0, maxZoom: 20, style: mapStyle, @@ -152,8 +140,6 @@ class _GeoMapState extends State { : EntryLeafletMap( controller: widget.controller, boundsNotifier: _boundsNotifier, - interactive: interactive, - showBackButton: showBackButton, minZoom: 2, maxZoom: 16, style: mapStyle, @@ -167,6 +153,7 @@ class _GeoMapState extends State { onMarkerTap: _onMarkerTap, ); + final mapHeight = context.select((v) => v.mapHeight); child = Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -192,11 +179,8 @@ class _GeoMapState extends State { } Widget replacement = Stack( children: [ - MapDecorator( - interactive: interactive, - ), + const MapDecorator(), MapButtonPanel( - showBackButton: showBackButton, boundsNotifier: _boundsNotifier, ), ], diff --git a/lib/widgets/common/map/google/map.dart b/lib/widgets/common/map/google/map.dart index ec21a072c..e0be2d050 100644 --- a/lib/widgets/common/map/google/map.dart +++ b/lib/widgets/common/map/google/map.dart @@ -10,15 +10,16 @@ import 'package:aves/widgets/common/map/decorator.dart'; import 'package:aves/widgets/common/map/geo_entry.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/google/marker_generator.dart'; +import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:latlong2/latlong.dart' as ll; +import 'package:provider/provider.dart'; class EntryGoogleMap extends StatefulWidget { final AvesMapController? controller; final ValueNotifier boundsNotifier; - final bool interactive, showBackButton; final double? minZoom, maxZoom; final EntryMapStyle style; final MarkerClusterBuilder markerClusterBuilder; @@ -30,8 +31,6 @@ class EntryGoogleMap extends StatefulWidget { Key? key, this.controller, required this.boundsNotifier, - required this.interactive, - required this.showBackButton, this.minZoom, this.maxZoom, required this.style, @@ -56,8 +55,6 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse ZoomedBounds get bounds => boundsNotifier.value; - bool get interactive => widget.interactive; - static const uninitializedLatLng = LatLng(0, 0); @override @@ -123,14 +120,12 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse }, ), MapDecorator( - interactive: interactive, child: _buildMap(), ), MapButtonPanel( - showBackButton: widget.showBackButton, boundsNotifier: boundsNotifier, zoomBy: _zoomBy, - resetRotation: interactive ? _resetRotation : null, + resetRotation: _resetRotation, ), ], ); @@ -155,7 +150,7 @@ class _EntryGoogleMapState extends State with WidgetsBindingObse } }); - final interactive = widget.interactive; + final interactive = context.select((v) => v.interactive); return GoogleMap( initialCameraPosition: CameraPosition( target: _toGoogleLatLng(bounds.center), diff --git a/lib/widgets/common/map/leaflet/map.dart b/lib/widgets/common/map/leaflet/map.dart index d35daed2a..b2c72a771 100644 --- a/lib/widgets/common/map/leaflet/map.dart +++ b/lib/widgets/common/map/leaflet/map.dart @@ -11,15 +11,16 @@ import 'package:aves/widgets/common/map/geo_map.dart'; import 'package:aves/widgets/common/map/latlng_tween.dart'; import 'package:aves/widgets/common/map/leaflet/scale_layer.dart'; import 'package:aves/widgets/common/map/leaflet/tile_layers.dart'; +import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves/widgets/common/map/zoomed_bounds.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:latlong2/latlong.dart'; +import 'package:provider/provider.dart'; class EntryLeafletMap extends StatefulWidget { final AvesMapController? controller; final ValueNotifier boundsNotifier; - final bool interactive, showBackButton; final double minZoom, maxZoom; final EntryMapStyle style; final MarkerClusterBuilder markerClusterBuilder; @@ -32,8 +33,6 @@ class EntryLeafletMap extends StatefulWidget { Key? key, this.controller, required this.boundsNotifier, - required this.interactive, - required this.showBackButton, this.minZoom = 0, this.maxZoom = 22, required this.style, @@ -58,8 +57,6 @@ class _EntryLeafletMapState extends State with TickerProviderSt ZoomedBounds get bounds => boundsNotifier.value; - bool get interactive => widget.interactive; - // duration should match the uncustomizable Google Maps duration static const _cameraAnimationDuration = Duration(milliseconds: 600); @@ -104,14 +101,12 @@ class _EntryLeafletMapState extends State with TickerProviderSt return Stack( children: [ MapDecorator( - interactive: interactive, child: _buildMap(), ), MapButtonPanel( - showBackButton: widget.showBackButton, boundsNotifier: boundsNotifier, zoomBy: _zoomBy, - resetRotation: interactive ? _resetRotation : null, + resetRotation: _resetRotation, ), ], ); @@ -135,13 +130,14 @@ class _EntryLeafletMapState extends State with TickerProviderSt ); }).toList(); + final interactive = context.select((v) => v.interactive); return FlutterMap( options: MapOptions( center: bounds.center, zoom: bounds.zoom, minZoom: widget.minZoom, maxZoom: widget.maxZoom, - interactiveFlags: widget.interactive ? InteractiveFlag.all : InteractiveFlag.none, + interactiveFlags: interactive ? InteractiveFlag.all : InteractiveFlag.none, controller: _leafletMapController, ), mapController: _leafletMapController, diff --git a/lib/widgets/common/map/theme.dart b/lib/widgets/common/map/theme.dart new file mode 100644 index 000000000..22d064d71 --- /dev/null +++ b/lib/widgets/common/map/theme.dart @@ -0,0 +1,53 @@ +import 'package:aves/model/settings/settings.dart'; +import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +enum MapNavigationButton { back, map } + +class MapTheme extends StatelessWidget { + final bool interactive; + final MapNavigationButton navigationButton; + final VisualDensity? visualDensity; + final double? mapHeight; + final Widget child; + + const MapTheme({ + Key? key, + required this.interactive, + required this.navigationButton, + this.visualDensity, + this.mapHeight, + required this.child, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + return ProxyProvider( + update: (_, settings, __) { + return MapThemeData( + interactive: interactive, + navigationButton: navigationButton, + visualDensity: visualDensity, + mapHeight: mapHeight, + // TODO TLAD use settings? + // showLocation: showBackButton ?? settings.showThumbnailLocation, + ); + }, + child: child, + ); + } +} + +class MapThemeData { + final bool interactive; + final MapNavigationButton navigationButton; + final VisualDensity? visualDensity; + final double? mapHeight; + + const MapThemeData({ + required this.interactive, + required this.navigationButton, + this.visualDensity, + this.mapHeight, + }); +} diff --git a/lib/widgets/map/map_page.dart b/lib/widgets/map/map_page.dart index df11c0af0..a658f462e 100644 --- a/lib/widgets/map/map_page.dart +++ b/lib/widgets/map/map_page.dart @@ -5,6 +5,7 @@ import 'package:aves/theme/durations.dart'; import 'package:aves/utils/debouncer.dart'; import 'package:aves/widgets/common/map/controller.dart'; import 'package:aves/widgets/common/map/geo_map.dart'; +import 'package:aves/widgets/common/map/theme.dart'; import 'package:aves/widgets/common/providers/media_query_data_provider.dart'; import 'package:aves/widgets/common/thumbnail/scroller.dart'; import 'package:flutter/foundation.dart'; @@ -65,20 +66,22 @@ class _MapPageState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ Expanded( - child: GeoMap( - controller: _mapController, - entries: entries, + child: MapTheme( interactive: true, - showBackButton: true, - isAnimatingNotifier: _isAnimatingNotifier, - onMarkerTap: (markerEntry, getClusterEntries) { - final index = entries.indexOf(markerEntry); - if (_selectedIndexNotifier.value != index) { - _selectedIndexNotifier.value = index; - } else { - _moveToEntry(markerEntry); - } - }, + navigationButton: MapNavigationButton.back, + child: GeoMap( + controller: _mapController, + entries: entries, + isAnimatingNotifier: _isAnimatingNotifier, + onMarkerTap: (markerEntry, getClusterEntries) { + final index = entries.indexOf(markerEntry); + if (_selectedIndexNotifier.value != index) { + _selectedIndexNotifier.value = index; + } else { + _moveToEntry(markerEntry); + } + }, + ), ), ), const Divider(), diff --git a/lib/widgets/viewer/info/location_section.dart b/lib/widgets/viewer/info/location_section.dart index 56290a20b..25cdf0db9 100644 --- a/lib/widgets/viewer/info/location_section.dart +++ b/lib/widgets/viewer/info/location_section.dart @@ -8,8 +8,10 @@ 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/theme.dart'; import 'package:aves/widgets/viewer/info/common.dart'; import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; class LocationSection extends StatefulWidget { final CollectionLens? collection; @@ -82,13 +84,19 @@ class _LocationSectionState extends State { crossAxisAlignment: CrossAxisAlignment.start, children: [ if (widget.showTitle) const SectionRow(icon: AIcons.location), - GeoMap( - entries: [entry], - interactive: false, - showBackButton: false, - mapHeight: 200, - isAnimatingNotifier: widget.isScrollingNotifier, - onUserZoomChange: (zoom) => settings.infoMapZoom = zoom, + ChangeNotifierProvider.value( + value: collection, + child: MapTheme( + interactive: false, + navigationButton: MapNavigationButton.map, + visualDensity: VisualDensity.compact, + mapHeight: 200, + child: GeoMap( + entries: [entry], + isAnimatingNotifier: widget.isScrollingNotifier, + onUserZoomChange: (zoom) => settings.infoMapZoom = zoom, + ), + ), ), _AddressInfoGroup(entry: entry), if (filters.isNotEmpty)