diff --git a/lib/widgets/common/action_delegates/map_style_dialog.dart b/lib/widgets/common/action_delegates/map_style_dialog.dart new file mode 100644 index 000000000..a9b0fd3d2 --- /dev/null +++ b/lib/widgets/common/action_delegates/map_style_dialog.dart @@ -0,0 +1,51 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; + +import '../dialog.dart'; + +class MapStyleDialog extends StatefulWidget { + @override + _MapStyleDialogState createState() => _MapStyleDialogState(); +} + +class _MapStyleDialogState extends State { + EntryMapStyle _selectedStyle; + + @override + void initState() { + super.initState(); + _selectedStyle = settings.infoMapStyle; + } + + @override + Widget build(BuildContext context) { + return AvesDialog( + title: 'Map Style', + scrollableContent: EntryMapStyle.values.map((style) => _buildRadioListTile(style, style.name)).toList(), + actions: [ + FlatButton( + onPressed: () => Navigator.pop(context), + child: Text('Cancel'.toUpperCase()), + ), + FlatButton( + onPressed: () => Navigator.pop(context, _selectedStyle), + child: Text('Apply'.toUpperCase()), + ), + ], + ); + } + + Widget _buildRadioListTile(EntryMapStyle style, String title) => RadioListTile( + value: style, + groupValue: _selectedStyle, + onChanged: (style) => setState(() => _selectedStyle = style), + title: Text( + title, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + ); +} diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index a1de125b7..43e68b3bc 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -46,6 +46,7 @@ class AIcons { static const IconData share = OMIcons.share; static const IconData sort = OMIcons.sort; static const IconData stats = OMIcons.pieChart; + static const IconData style = OMIcons.palette; static const IconData zoomIn = OMIcons.add; static const IconData zoomOut = OMIcons.remove; diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 4dac6a6ba..23b7ada6b 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -6,6 +6,7 @@ import 'package:aves/utils/geo_utils.dart'; import 'package:aves/widgets/common/aves_filter_chip.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/info_page.dart'; +import 'package:aves/widgets/fullscreen/info/maps/buttons.dart'; import 'package:aves/widgets/fullscreen/info/maps/google_map.dart'; import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart'; import 'package:flutter/material.dart'; @@ -94,20 +95,25 @@ class _LocationSectionState extends State { padding: EdgeInsets.only(bottom: 8), child: SectionRow(AIcons.location), ), - if (settings.infoMapStyle == EntryMapStyle.google) - EntryGoogleMap( - markerId: entry.uri ?? entry.path, - latLng: entry.latLng, - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - ) - else - EntryLeafletMap( - latLng: entry.latLng, - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - style: settings.infoMapStyle, - ), + NotificationListener( + onNotification: (notification) { + if (notification is MapStyleChangedNotification) setState(() {}); + return false; + }, + child: settings.infoMapStyle == EntryMapStyle.google + ? EntryGoogleMap( + markerId: entry.uri ?? entry.path, + latLng: entry.latLng, + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + ) + : EntryLeafletMap( + latLng: entry.latLng, + geoUri: entry.geoUri, + initialZoom: settings.infoMapZoom, + style: settings.infoMapStyle, + ), + ), if (location.isNotEmpty) Padding( padding: EdgeInsets.only(top: 8), diff --git a/lib/widgets/fullscreen/info/maps/buttons.dart b/lib/widgets/fullscreen/info/maps/buttons.dart new file mode 100644 index 000000000..61e5a7929 --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/buttons.dart @@ -0,0 +1,112 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/services/android_app_service.dart'; +import 'package:aves/widgets/common/action_delegates/map_style_dialog.dart'; +import 'package:aves/widgets/common/fx/blurred.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; +import 'package:aves/widgets/fullscreen/overlay/common.dart'; +import 'package:flutter/material.dart'; + +class MapButtonPanel extends StatelessWidget { + final String geoUri; + final void Function(double amount) zoomBy; + + static const BorderRadius mapBorderRadius = BorderRadius.all(Radius.circular(24)); // to match button circles + static const double padding = 4; + + const MapButtonPanel({ + @required this.geoUri, + @required this.zoomBy, + }); + + @override + Widget build(BuildContext context) { + return Positioned.fill( + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: Padding( + padding: EdgeInsets.all(padding), + child: TooltipTheme( + data: TooltipTheme.of(context).copyWith( + preferBelow: false, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + MapOverlayButton( + icon: AIcons.style, + onPressed: () async { + final style = await showDialog( + context: context, + builder: (context) => MapStyleDialog(), + ); + if (style != null) { + settings.infoMapStyle = style; + MapStyleChangedNotification().dispatch(context); + } + }, + tooltip: 'Style map...', + ), + SizedBox(height: padding), + MapOverlayButton( + icon: AIcons.openInNew, + onPressed: () => AndroidAppService.openMap(geoUri), + tooltip: 'Show on map...', + ), + Spacer(), + MapOverlayButton( + icon: AIcons.zoomIn, + onPressed: () => zoomBy(1), + tooltip: 'Zoom in', + ), + SizedBox(height: padding), + MapOverlayButton( + icon: AIcons.zoomOut, + onPressed: () => zoomBy(-1), + tooltip: 'Zoom out', + ), + ], + ), + ), + ), + ), + ); + } +} + +class MapOverlayButton extends StatelessWidget { + final IconData icon; + final String tooltip; + final VoidCallback onPressed; + + const MapOverlayButton({ + @required this.icon, + @required this.tooltip, + @required this.onPressed, + }); + + @override + Widget build(BuildContext context) { + return BlurredOval( + child: Material( + type: MaterialType.circle, + color: FullscreenOverlay.backgroundColor, + child: Ink( + decoration: BoxDecoration( + border: FullscreenOverlay.buildBorder(context), + shape: BoxShape.circle, + ), + child: IconButton( + iconSize: 20, + visualDensity: VisualDensity.compact, + icon: Icon(icon), + onPressed: onPressed, + tooltip: tooltip, + ), + ), + ), + ); + } +} + +class MapStyleChangedNotification extends Notification {} diff --git a/lib/widgets/fullscreen/info/maps/google_map.dart b/lib/widgets/fullscreen/info/maps/google_map.dart index fb23ae4af..4b59edb35 100644 --- a/lib/widgets/fullscreen/info/maps/google_map.dart +++ b/lib/widgets/fullscreen/info/maps/google_map.dart @@ -1,6 +1,5 @@ import 'package:aves/model/settings.dart'; -import 'package:aves/services/android_app_service.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/fullscreen/info/maps/buttons.dart'; import 'package:flutter/material.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tuple/tuple.dart'; @@ -38,74 +37,62 @@ class EntryGoogleMapState extends State with AutomaticKeepAliveC @override Widget build(BuildContext context) { super.build(context); - final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; - return Row( + return Stack( children: [ - Expanded( - child: GestureDetector( - onScaleStart: (details) { - // absorb scale gesture here to prevent scrolling - // and triggering by mistake a move to the image page above - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Container( - color: Colors.white70, - height: 200, - child: GoogleMap( - // GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493 - initialCameraPosition: CameraPosition( - target: widget.latLng, - zoom: widget.initialZoom, - ), - onMapCreated: (controller) => setState(() => _controller = controller), - rotateGesturesEnabled: false, - scrollGesturesEnabled: false, - zoomControlsEnabled: false, - zoomGesturesEnabled: false, - liteModeEnabled: false, - tiltGesturesEnabled: false, - myLocationEnabled: false, - myLocationButtonEnabled: false, - markers: { - Marker( - markerId: MarkerId(widget.markerId), - icon: BitmapDescriptor.defaultMarkerWithHue(accentHue), - position: widget.latLng, - ) - }, - ), - ), + _buildMap(), + Positioned.fill( + child: Align( + alignment: AlignmentDirectional.centerEnd, + child: MapButtonPanel( + geoUri: widget.geoUri, + zoomBy: _zoomBy, ), ), - ), - SizedBox(width: 8), - TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: Column(children: [ - IconButton( - icon: Icon(AIcons.zoomIn), - onPressed: _controller == null ? null : () => _zoomBy(1), - tooltip: 'Zoom in', - ), - IconButton( - icon: Icon(AIcons.zoomOut), - onPressed: _controller == null ? null : () => _zoomBy(-1), - tooltip: 'Zoom out', - ), - IconButton( - icon: Icon(AIcons.openInNew), - onPressed: () => AndroidAppService.openMap(widget.geoUri), - tooltip: 'Show on map...', - ), - ]), ) ], ); } + Widget _buildMap() { + final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; + return GestureDetector( + onScaleStart: (details) { + // absorb scale gesture here to prevent scrolling + // and triggering by mistake a move to the image page above + }, + child: ClipRRect( + borderRadius: MapButtonPanel.mapBorderRadius, + child: Container( + color: Colors.white70, + height: 200, + child: GoogleMap( + // GoogleMap init perf issue: https://github.com/flutter/flutter/issues/28493 + initialCameraPosition: CameraPosition( + target: widget.latLng, + zoom: widget.initialZoom, + ), + onMapCreated: (controller) => setState(() => _controller = controller), + rotateGesturesEnabled: false, + scrollGesturesEnabled: false, + zoomControlsEnabled: false, + zoomGesturesEnabled: false, + liteModeEnabled: false, + tiltGesturesEnabled: false, + myLocationEnabled: false, + myLocationButtonEnabled: false, + markers: { + Marker( + markerId: MarkerId(widget.markerId), + icon: BitmapDescriptor.defaultMarkerWithHue(accentHue), + position: widget.latLng, + ) + }, + ), + ), + ), + ); + } + void _zoomBy(double amount) { settings.infoMapZoom += amount; _controller.animateCamera(CameraUpdate.zoomBy(amount)); diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart index 464a46607..a75c05976 100644 --- a/lib/widgets/fullscreen/info/maps/leaflet_map.dart +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -1,6 +1,5 @@ import 'package:aves/model/settings.dart'; -import 'package:aves/services/android_app_service.dart'; -import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/fullscreen/info/maps/buttons.dart'; import 'package:aves/widgets/fullscreen/info/maps/scale_layer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; @@ -46,83 +45,16 @@ class EntryLeafletMapState extends State with AutomaticKeepAliv @override Widget build(BuildContext context) { super.build(context); - final accentColor = Theme.of(context).accentColor; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Row( + Stack( children: [ - Expanded( - child: GestureDetector( - onScaleStart: (details) { - // absorb scale gesture here to prevent scrolling - // and triggering by mistake a move to the image page above - }, - child: ClipRRect( - borderRadius: BorderRadius.circular(16), - child: Container( - color: Colors.white70, - height: 200, - child: FlutterMap( - options: MapOptions( - center: widget.latLng, - zoom: widget.initialZoom, - interactive: false, - ), - children: [ - _buildMapLayer(), - ScaleLayerWidget( - options: ScaleLayerOptions(), - ), - MarkerLayerWidget( - options: MarkerLayerOptions( - markers: [ - Marker( - width: markerSize, - height: markerSize, - point: widget.latLng, - builder: (ctx) { - return Icon( - Icons.place, - size: markerSize, - color: accentColor, - ); - }, - anchorPos: AnchorPos.align(AnchorAlign.top), - ), - ], - ), - ), - ], - mapController: _mapController, - ), - ), - ), - ), + _buildMap(), + MapButtonPanel( + geoUri: widget.geoUri, + zoomBy: _zoomBy, ), - SizedBox(width: 8), - TooltipTheme( - data: TooltipTheme.of(context).copyWith( - preferBelow: false, - ), - child: Column(children: [ - IconButton( - icon: Icon(AIcons.zoomIn), - onPressed: _mapController == null ? null : () => _zoomBy(1), - tooltip: 'Zoom in', - ), - IconButton( - icon: Icon(AIcons.zoomOut), - onPressed: _mapController == null ? null : () => _zoomBy(-1), - tooltip: 'Zoom out', - ), - IconButton( - icon: Icon(AIcons.openInNew), - onPressed: () => AndroidAppService.openMap(widget.geoUri), - tooltip: 'Show on map...', - ), - ]), - ) ], ), _buildAttribution(), @@ -130,6 +62,55 @@ class EntryLeafletMapState extends State with AutomaticKeepAliv ); } + Widget _buildMap() { + return GestureDetector( + onScaleStart: (details) { + // absorb scale gesture here to prevent scrolling + // and triggering by mistake a move to the image page above + }, + child: ClipRRect( + borderRadius: MapButtonPanel.mapBorderRadius, + child: Container( + color: Colors.white70, + height: 200, + child: FlutterMap( + options: MapOptions( + center: widget.latLng, + zoom: widget.initialZoom, + interactive: false, + ), + children: [ + _buildMapLayer(), + ScaleLayerWidget( + options: ScaleLayerOptions(), + ), + MarkerLayerWidget( + options: MarkerLayerOptions( + markers: [ + Marker( + width: markerSize, + height: markerSize, + point: widget.latLng, + builder: (ctx) { + return Icon( + Icons.place, + size: markerSize, + color: Theme.of(context).accentColor, + ); + }, + anchorPos: AnchorPos.align(AnchorAlign.top), + ), + ], + ), + ), + ], + mapController: _mapController, + ), + ), + ), + ); + } + Widget _buildMapLayer() { switch (widget.style) { case EntryMapStyle.osmHot: @@ -144,39 +125,38 @@ class EntryLeafletMapState extends State with AutomaticKeepAliv } Widget _buildAttribution() { - final attribution = _getAttributionMarkdown(); - return attribution != null - ? Markdown( - data: attribution, - selectable: true, - styleSheet: MarkdownStyleSheet( - a: TextStyle(color: Theme.of(context).accentColor), - p: TextStyle(fontSize: 13, fontFamily: 'Concourse'), - ), - onTapLink: (url) async { - if (await canLaunch(url)) { - await launch(url); - } - }, - padding: EdgeInsets.symmetric(vertical: 4, horizontal: 8), - shrinkWrap: true, - ) - : SizedBox.shrink(); - } - - String _getAttributionMarkdown() { switch (widget.style) { case EntryMapStyle.osmHot: - return '© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, Tiles style by [Humanitarian OpenStreetMap Team](https://www.hotosm.org/) hosted by [OpenStreetMap France](https://openstreetmap.fr/)'; + return _buildAttributionMarkdown('© [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors, Tiles style by [Humanitarian OpenStreetMap Team](https://www.hotosm.org/) hosted by [OpenStreetMap France](https://openstreetmap.fr/)'); case EntryMapStyle.stamenToner: case EntryMapStyle.stamenWatercolor: - return 'Map tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0) — Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors'; + return _buildAttributionMarkdown('Map tiles by [Stamen Design](http://stamen.com), [CC BY 3.0](http://creativecommons.org/licenses/by/3.0) — Map data © [OpenStreetMap](https://www.openstreetmap.org/copyright) contributors'); default: - return null; + return SizedBox.shrink(); } } + Widget _buildAttributionMarkdown(String data) { + return Markdown( + data: data, + selectable: true, + styleSheet: MarkdownStyleSheet( + a: TextStyle(color: Theme.of(context).accentColor), + p: TextStyle(color: Colors.white70, fontSize: 13, fontFamily: 'Concourse'), + ), + onTapLink: (url) async { + if (await canLaunch(url)) { + await launch(url); + } + }, + padding: EdgeInsets.symmetric(vertical: 4, horizontal: 0), + shrinkWrap: true, + ); + } + void _zoomBy(double amount) { + if (_mapController == null) return; + final endZoom = (settings.infoMapZoom + amount).clamp(1.0, 16.0); settings.infoMapZoom = endZoom; diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index b211ddec5..4ff3fd2d1 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -102,7 +102,7 @@ class _HomePageState extends State { return SingleFullscreenPage(entry: _viewerEntry); } if (_mediaStore != null) { - switch(settings.launchPage) { + switch (settings.launchPage) { case LaunchPage.albums: return AlbumListPage(source: _mediaStore); break; diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart index 8a0b2af4f..bd94b32f1 100644 --- a/lib/widgets/settings_page.dart +++ b/lib/widgets/settings_page.dart @@ -1,7 +1,6 @@ import 'package:aves/model/settings.dart'; import 'package:aves/utils/constants.dart'; import 'package:aves/widgets/common/data_providers/media_query_data_provider.dart'; -import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:flutter/material.dart'; class SettingsPage extends StatelessWidget { @@ -27,16 +26,6 @@ class SettingsPage extends StatelessWidget { Flexible(child: LaunchPageSelector()), ], ), - SizedBox(height: 16), - Text('Maps', style: Constants.titleTextStyle), - Row( - mainAxisSize: MainAxisSize.min, - children: [ - Text('Storage:'), - SizedBox(width: 8), - Flexible(child: InfoMapStyleSelector()), - ], - ), ], ), ), @@ -46,35 +35,6 @@ class SettingsPage extends StatelessWidget { } } -class InfoMapStyleSelector extends StatefulWidget { - @override - _InfoMapStyleSelectorState createState() => _InfoMapStyleSelectorState(); -} - -class _InfoMapStyleSelectorState extends State { - @override - Widget build(BuildContext context) { - return DropdownButton( - items: EntryMapStyle.values - .map((selected) => DropdownMenuItem( - value: selected, - child: Text( - selected.name, - softWrap: false, - overflow: TextOverflow.fade, - maxLines: 1, - ), - )) - .toList(), - value: settings.infoMapStyle, - onChanged: (selected) { - settings.infoMapStyle = selected; - setState(() {}); - }, - ); - } -} - class LaunchPageSelector extends StatefulWidget { @override _LaunchPageSelectorState createState() => _LaunchPageSelectorState();