From fe40408c07debadd8111aa11113bdac92cbb2637 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 9 Aug 2020 14:53:14 +0900 Subject: [PATCH 01/10] info: alternate map styles --- lib/flutter_version.dart | 2 + lib/model/settings.dart | 6 + lib/widgets/app_drawer.dart | 21 ++ lib/widgets/common/icons.dart | 1 + .../fullscreen/info/location_section.dart | 145 +++--------- .../fullscreen/info/maps/google_map.dart | 118 ++++++++++ .../fullscreen/info/maps/leaflet_map.dart | 216 ++++++++++++++++++ .../fullscreen/info/maps/scale_layer.dart | 141 ++++++++++++ .../fullscreen/info/maps/scalebar_utils.dart | 126 ++++++++++ lib/widgets/settings_page.dart | 65 ++++++ pubspec.lock | 140 ++++++++++++ pubspec.yaml | 2 + 12 files changed, 867 insertions(+), 116 deletions(-) create mode 100644 lib/widgets/fullscreen/info/maps/google_map.dart create mode 100644 lib/widgets/fullscreen/info/maps/leaflet_map.dart create mode 100644 lib/widgets/fullscreen/info/maps/scale_layer.dart create mode 100644 lib/widgets/fullscreen/info/maps/scalebar_utils.dart create mode 100644 lib/widgets/settings_page.dart diff --git a/lib/flutter_version.dart b/lib/flutter_version.dart index bb9c69953..cee1bcb93 100644 --- a/lib/flutter_version.dart +++ b/lib/flutter_version.dart @@ -1,4 +1,6 @@ // run `scripts/update_flutter_version.sh` to update with the content of `flutter --version --machine` +// note on static analysis: the output of the script above yields double quotes, just like the example below +// ignore_for_file: prefer_single_quotes const Map version = { "channel": "unknown", "dartSdkVersion": "unknown", diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 7de40ad4c..801340b09 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -1,4 +1,5 @@ import 'package:aves/model/source/collection_lens.dart'; +import 'package:aves/widgets/fullscreen/info/location_section.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/widgets.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -18,6 +19,7 @@ class Settings { static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionTileExtentKey = 'collection_tile_extent'; + static const infoMapStyleKey = 'info_map_style'; static const infoMapZoomKey = 'info_map_zoom'; static const catalogTimeZoneKey = 'catalog_time_zone'; static const hasAcceptedTermsKey = 'has_accepted_terms'; @@ -50,6 +52,10 @@ class Settings { } } + EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); + + set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); + double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index e9eac9d66..c0ee4e7f3 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -21,6 +21,7 @@ import 'package:aves/widgets/common/aves_logo.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/debug_page.dart'; import 'package:aves/widgets/filter_grid_page.dart'; +import 'package:aves/widgets/settings_page.dart'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; @@ -93,6 +94,15 @@ class _AppDrawerState extends State { title: 'Favourites', filter: FavouriteFilter(), ); + final settingsEntry = SafeArea( + top: false, + bottom: false, + child: ListTile( + leading: Icon(AIcons.settings), + title: Text('Preferences'), + onTap: () => _goToSettings(context), + ), + ); final aboutEntry = SafeArea( top: false, bottom: false, @@ -114,6 +124,7 @@ class _AppDrawerState extends State { _buildCountrySection(), _buildTagSection(), Divider(), + settingsEntry, aboutEntry, if (kDebugMode) ...[ Divider(), @@ -310,6 +321,16 @@ class _AppDrawerState extends State { ); } + void _goToSettings(BuildContext context) { + Navigator.pop(context); + Navigator.push( + context, + MaterialPageRoute( + builder: (context) => SettingsPage(), + ), + ); + } + void _goToAbout(BuildContext context) { Navigator.pop(context); Navigator.push( diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index 1c10cdcaf..e08962239 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -19,6 +19,7 @@ class AIcons { static const IconData location = OMIcons.place; static const IconData shooting = OMIcons.camera; static const IconData removableStorage = OMIcons.sdStorage; + static const IconData settings = OMIcons.settings; static const IconData text = OMIcons.formatQuote; static const IconData tag = OMIcons.localOffer; diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 26b57c96f..380bdbc59 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -2,13 +2,13 @@ import 'package:aves/model/filters/location.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/source/collection_lens.dart'; -import 'package:aves/services/android_app_service.dart'; 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/google_map.dart'; +import 'package:aves/widgets/fullscreen/info/maps/leaflet_map.dart'; import 'package:flutter/material.dart'; -import 'package:google_maps_flutter/google_maps_flutter.dart'; class LocationSection extends StatefulWidget { final CollectionLens collection; @@ -94,15 +94,20 @@ class _LocationSectionState extends State { padding: EdgeInsets.only(bottom: 8), child: SectionRow(AIcons.location), ), - ImageMap( - markerId: entry.uri ?? entry.path, - latLng: LatLng( - entry.latLng.item1, - entry.latLng.item2, + 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, ), - geoUri: entry.geoUri, - initialZoom: settings.infoMapZoom, - ), if (location.isNotEmpty) Padding( padding: EdgeInsets.only(top: 8), @@ -133,113 +138,21 @@ class _LocationSectionState extends State { void _handleChange() => setState(() {}); } -class ImageMap extends StatefulWidget { - final String markerId; - final LatLng latLng; - final String geoUri; - final double initialZoom; +enum EntryMapStyle { google, osmHot, stamenToner, stamenWatercolor } - const ImageMap({ - Key key, - this.markerId, - this.latLng, - this.geoUri, - this.initialZoom, - }) : super(key: key); - - @override - State createState() => ImageMapState(); -} - -class ImageMapState extends State with AutomaticKeepAliveClientMixin { - GoogleMapController _controller; - - @override - void didUpdateWidget(ImageMap oldWidget) { - super.didUpdateWidget(oldWidget); - if (widget.latLng != oldWidget.latLng && _controller != null) { - _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); +extension ExtraEntryMapStyle on EntryMapStyle { + String get name { + switch (this) { + case EntryMapStyle.google: + return 'Google Maps'; + case EntryMapStyle.osmHot: + return 'Humanitarian OpenStreetMap'; + case EntryMapStyle.stamenToner: + return 'Stamen Toner'; + case EntryMapStyle.stamenWatercolor: + return 'Stamen Watercolor'; + default: + return toString(); } } - - @override - Widget build(BuildContext context) { - super.build(context); - final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; - return Row( - 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.all( - Radius.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, - ) - }, - ), - ), - ), - ), - ), - 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...', - ), - ]), - ) - ], - ); - } - - void _zoomBy(double amount) { - settings.infoMapZoom += amount; - _controller.animateCamera(CameraUpdate.zoomBy(amount)); - } - - @override - bool get wantKeepAlive => true; } diff --git a/lib/widgets/fullscreen/info/maps/google_map.dart b/lib/widgets/fullscreen/info/maps/google_map.dart new file mode 100644 index 000000000..39fae754a --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/google_map.dart @@ -0,0 +1,118 @@ +import 'package:aves/model/settings.dart'; +import 'package:aves/services/android_app_service.dart'; +import 'package:aves/widgets/common/icons.dart'; +import 'package:flutter/material.dart'; +import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:tuple/tuple.dart'; + +class EntryGoogleMap extends StatefulWidget { + final String markerId; + final LatLng latLng; + final String geoUri; + final double initialZoom; + + EntryGoogleMap({ + Key key, + this.markerId, + Tuple2 latLng, + this.geoUri, + this.initialZoom, + }) : latLng = LatLng(latLng.item1, latLng.item2), + super(key: key); + + @override + State createState() => EntryGoogleMapState(); +} + +class EntryGoogleMapState extends State with AutomaticKeepAliveClientMixin { + GoogleMapController _controller; + + @override + void didUpdateWidget(EntryGoogleMap oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.latLng != oldWidget.latLng && _controller != null) { + _controller.moveCamera(CameraUpdate.newLatLng(widget.latLng)); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + final accentHue = HSVColor.fromColor(Theme.of(context).accentColor).hue; + return Row( + 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.all( + Radius.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, + ) + }, + ), + ), + ), + ), + ), + 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...', + ), + ]), + ) + ], + ); + } + + void _zoomBy(double amount) { + settings.infoMapZoom += amount; + _controller.animateCamera(CameraUpdate.zoomBy(amount)); + } + + @override + bool get wantKeepAlive => true; +} diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart new file mode 100644 index 000000000..de913348f --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -0,0 +1,216 @@ +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/scale_layer.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong/latlong.dart'; +import 'package:tuple/tuple.dart'; + +import '../location_section.dart'; + +class EntryLeafletMap extends StatefulWidget { + final LatLng latLng; + final String geoUri; + final double initialZoom; + final EntryMapStyle style; + + EntryLeafletMap({ + Key key, + Tuple2 latLng, + this.geoUri, + this.initialZoom, + this.style, + }) : latLng = LatLng(latLng.item1, latLng.item2), + super(key: key); + + @override + State createState() => EntryLeafletMapState(); +} + +class EntryLeafletMapState extends State with AutomaticKeepAliveClientMixin, TickerProviderStateMixin { + final MapController _mapController = MapController(); + + static const markerSize = 40.0; + + @override + void didUpdateWidget(EntryLeafletMap oldWidget) { + super.didUpdateWidget(oldWidget); + if (widget.latLng != oldWidget.latLng && _mapController != null) { + _mapController.move(widget.latLng, settings.infoMapZoom); + } + } + + @override + Widget build(BuildContext context) { + super.build(context); + final accentColor = Theme.of(context).accentColor; + return Row( + 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.all( + Radius.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( + lineColor: accentColor, + lineWidth: 2, + textStyle: TextStyle( + color: accentColor, + fontSize: 12, + ), + padding: EdgeInsets.all(8), + ), + ), + 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, + ), + ), + ), + ), + ), + 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...', + ), + ]), + ) + ], + ); + } + + Widget _buildMapLayer() { + switch (widget.style) { + case EntryMapStyle.osmHot: + return OSMHotLayer(); + case EntryMapStyle.stamenToner: + return StamenTonerLayer(); + case EntryMapStyle.stamenWatercolor: + return StamenWatercolorLayer(); + default: + return SizedBox.shrink(); + } + } + + void _zoomBy(double amount) { + final endZoom = (settings.infoMapZoom + amount).clamp(1.0, 16.0); + settings.infoMapZoom = endZoom; + + final zoomTween = Tween(begin: _mapController.zoom, end: endZoom); + final controller = AnimationController(duration: const Duration(milliseconds: 200), vsync: this); + final animation = CurvedAnimation(parent: controller, curve: Curves.fastOutSlowIn); + controller.addListener(() => _mapController.move(widget.latLng, zoomTween.evaluate(animation))); + animation.addStatusListener((status) { + if (status == AnimationStatus.completed) { + controller.dispose(); + } else if (status == AnimationStatus.dismissed) { + controller.dispose(); + } + }); + controller.forward(); + } + + @override + bool get wantKeepAlive => true; +} + +class OSMHotLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + // attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France' + minZoom: 1, + maxZoom: 19, + urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', + subdomains: ['a', 'b', 'c'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} + +class StamenTonerLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', + minZoom: 1, + maxZoom: 20, + urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', + subdomains: ['a', 'b', 'c', 'd'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} + +class StamenWatercolorLayer extends StatelessWidget { + @override + Widget build(BuildContext context) { + return TileLayerWidget( + options: TileLayerOptions( + // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', + minZoom: 1, + maxZoom: 16, + urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', + subdomains: ['a', 'b', 'c', 'd'], + retinaMode: MediaQuery.of(context).devicePixelRatio > 1, + ), + ); + } +} diff --git a/lib/widgets/fullscreen/info/maps/scale_layer.dart b/lib/widgets/fullscreen/info/maps/scale_layer.dart new file mode 100644 index 000000000..06cf170d2 --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/scale_layer.dart @@ -0,0 +1,141 @@ +import 'dart:math'; +import 'dart:ui' as ui; + +import 'package:flutter/material.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_map/plugin_api.dart'; + +import 'scalebar_utils.dart' as util; + +class ScaleLayerOptions extends LayerOptions { + TextStyle textStyle; + Color lineColor; + double lineWidth; + final EdgeInsets padding; + + ScaleLayerOptions({ + Key key, + this.textStyle, + this.lineColor = Colors.white, + this.lineWidth = 2, + this.padding, + rebuild, + }) : super(key: key, rebuild: rebuild); +} + +class ScaleLayerWidget extends StatelessWidget { + final ScaleLayerOptions options; + + ScaleLayerWidget({@required this.options}) : super(key: options.key); + + @override + Widget build(BuildContext context) { + final mapState = MapState.of(context); + return ScaleLayer(options, mapState, mapState.onMoved); + } +} + +class ScaleLayer extends StatelessWidget { + final ScaleLayerOptions scaleLayerOpts; + final MapState map; + final Stream stream; + final scale = [ + 25000000, + 15000000, + 8000000, + 4000000, + 2000000, + 1000000, + 500000, + 250000, + 100000, + 50000, + 25000, + 15000, + 8000, + 4000, + 2000, + 1000, + 500, + 250, + 100, + 50, + 25, + 10, + 5, + ]; + + ScaleLayer(this.scaleLayerOpts, this.map, this.stream) : super(key: scaleLayerOpts.key); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: stream, + builder: (context, snapshot) { + var zoom = map.zoom; + var distance = scale[max(0, min(20, zoom.round() + 2))].toDouble(); + var center = map.center; + var start = map.project(center); + var targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance); + var end = map.project(targetPoint); + var displayDistance = distance > 999 ? '${(distance / 1000).toStringAsFixed(0)} km' : '${distance.toStringAsFixed(0)} m'; + double width = (end.x - start.x); + + return CustomPaint( + painter: ScalePainter( + width, + displayDistance, + lineColor: scaleLayerOpts.lineColor, + lineWidth: scaleLayerOpts.lineWidth, + padding: scaleLayerOpts.padding, + textStyle: scaleLayerOpts.textStyle, + ), + ); + }, + ); + } +} + +class ScalePainter extends CustomPainter { + ScalePainter(this.width, this.text, {this.padding, this.textStyle, this.lineWidth, this.lineColor}); + + final double width; + final EdgeInsets padding; + final String text; + TextStyle textStyle; + double lineWidth; + Color lineColor; + + @override + void paint(ui.Canvas canvas, ui.Size size) { + final paint = Paint() + ..color = lineColor + ..strokeCap = StrokeCap.square + ..strokeWidth = lineWidth; + + var sizeForStartEnd = 4; + var paddingLeft = padding == null ? 0 : padding.left + sizeForStartEnd / 2; + var paddingTop = padding == null ? 0 : padding.top; + + var textSpan = TextSpan(style: textStyle, text: text); + var textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); + textPainter.paint(canvas, Offset(width / 2 - textPainter.width / 2 + paddingLeft, paddingTop)); + paddingTop += textPainter.height; + var p1 = Offset(paddingLeft, sizeForStartEnd + paddingTop); + var p2 = Offset(paddingLeft + width, sizeForStartEnd + paddingTop); + // draw start line + canvas.drawLine(Offset(paddingLeft, paddingTop), Offset(paddingLeft, sizeForStartEnd + paddingTop), paint); + // draw middle line + var middleX = width / 2 + paddingLeft - lineWidth / 2; + canvas.drawLine(Offset(middleX, paddingTop + sizeForStartEnd / 2), Offset(middleX, sizeForStartEnd + paddingTop), paint); + // draw end line + canvas.drawLine(Offset(width + paddingLeft, paddingTop), Offset(width + paddingLeft, sizeForStartEnd + paddingTop), paint); + // draw bottom line + canvas.drawLine(p1, p2, paint); + } + + @override + bool shouldRepaint(CustomPainter oldDelegate) { + return true; + } +} diff --git a/lib/widgets/fullscreen/info/maps/scalebar_utils.dart b/lib/widgets/fullscreen/info/maps/scalebar_utils.dart new file mode 100644 index 000000000..df781009a --- /dev/null +++ b/lib/widgets/fullscreen/info/maps/scalebar_utils.dart @@ -0,0 +1,126 @@ +import 'dart:math'; + +import 'package:latlong/latlong.dart'; + +const double piOver180 = PI / 180.0; + +double toDegrees(double radians) { + return radians / piOver180; +} + +double toRadians(double degrees) { + return degrees * piOver180; +} + +LatLng calculateEndingGlobalCoordinates(LatLng start, double startBearing, double distance) { + var mSemiMajorAxis = 6378137.0; //WGS84 major axis + var mSemiMinorAxis = (1.0 - 1.0 / 298.257223563) * 6378137.0; + var mFlattening = 1.0 / 298.257223563; + // double mInverseFlattening = 298.257223563; + + var a = mSemiMajorAxis; + var b = mSemiMinorAxis; + var aSquared = a * a; + var bSquared = b * b; + var f = mFlattening; + var phi1 = toRadians(start.latitude); + var alpha1 = toRadians(startBearing); + var cosAlpha1 = cos(alpha1); + var sinAlpha1 = sin(alpha1); + var s = distance; + var tanU1 = (1.0 - f) * tan(phi1); + var cosU1 = 1.0 / sqrt(1.0 + tanU1 * tanU1); + var sinU1 = tanU1 * cosU1; + + // eq. 1 + var sigma1 = atan2(tanU1, cosAlpha1); + + // eq. 2 + var sinAlpha = cosU1 * sinAlpha1; + + var sin2Alpha = sinAlpha * sinAlpha; + var cos2Alpha = 1 - sin2Alpha; + var uSquared = cos2Alpha * (aSquared - bSquared) / bSquared; + + // eq. 3 + var A = 1 + (uSquared / 16384) * (4096 + uSquared * (-768 + uSquared * (320 - 175 * uSquared))); + + // eq. 4 + var B = (uSquared / 1024) * (256 + uSquared * (-128 + uSquared * (74 - 47 * uSquared))); + + // iterate until there is a negligible change in sigma + double deltaSigma; + var sOverbA = s / (b * A); + var sigma = sOverbA; + double sinSigma; + var prevSigma = sOverbA; + double sigmaM2; + double cosSigmaM2; + double cos2SigmaM2; + + for (;;) { + // eq. 5 + sigmaM2 = 2.0 * sigma1 + sigma; + cosSigmaM2 = cos(sigmaM2); + cos2SigmaM2 = cosSigmaM2 * cosSigmaM2; + sinSigma = sin(sigma); + var cosSignma = cos(sigma); + + // eq. 6 + deltaSigma = B * sinSigma * (cosSigmaM2 + (B / 4.0) * (cosSignma * (-1 + 2 * cos2SigmaM2) - (B / 6.0) * cosSigmaM2 * (-3 + 4 * sinSigma * sinSigma) * (-3 + 4 * cos2SigmaM2))); + + // eq. 7 + sigma = sOverbA + deltaSigma; + + // break after converging to tolerance + if ((sigma - prevSigma).abs() < 0.0000000000001) break; + + prevSigma = sigma; + } + + sigmaM2 = 2.0 * sigma1 + sigma; + cosSigmaM2 = cos(sigmaM2); + cos2SigmaM2 = cosSigmaM2 * cosSigmaM2; + + var cosSigma = cos(sigma); + sinSigma = sin(sigma); + + // eq. 8 + var phi2 = atan2(sinU1 * cosSigma + cosU1 * sinSigma * cosAlpha1, (1.0 - f) * sqrt(sin2Alpha + pow(sinU1 * sinSigma - cosU1 * cosSigma * cosAlpha1, 2.0))); + + // eq. 9 + // This fixes the pole crossing defect spotted by Matt Feemster. When a + // path passes a pole and essentially crosses a line of latitude twice - + // once in each direction - the longitude calculation got messed up. + // Using + // atan2 instead of atan fixes the defect. The change is in the next 3 + // lines. + // double tanLambda = sinSigma * sinAlpha1 / (cosU1 * cosSigma - sinU1 * + // sinSigma * cosAlpha1); + // double lambda = Math.atan(tanLambda); + var lambda = atan2(sinSigma * sinAlpha1, (cosU1 * cosSigma - sinU1 * sinSigma * cosAlpha1)); + + // eq. 10 + var C = (f / 16) * cos2Alpha * (4 + f * (4 - 3 * cos2Alpha)); + + // eq. 11 + var L = lambda - (1 - C) * f * sinAlpha * (sigma + C * sinSigma * (cosSigmaM2 + C * cosSigma * (-1 + 2 * cos2SigmaM2))); + + // eq. 12 + // double alpha2 = Math.atan2(sinAlpha, -sinU1 * sinSigma + cosU1 * + // cosSigma * cosAlpha1); + + // build result + var latitude = toDegrees(phi2); + var longitude = start.longitude + toDegrees(L); + + // if ((endBearing != null) && (endBearing.length > 0)) { + // endBearing[0] = toDegrees(alpha2); + // } + + latitude = latitude < -90 ? -90 : latitude; + latitude = latitude > 90 ? 90 : latitude; + longitude = longitude < -180 ? -180 : longitude; + longitude = longitude > 180 ? 180 : longitude; + return LatLng(latitude, longitude); +} diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart new file mode 100644 index 000000000..f12b8cd1f --- /dev/null +++ b/lib/widgets/settings_page.dart @@ -0,0 +1,65 @@ +import 'package:aves/model/settings.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 { + @override + Widget build(BuildContext context) { + return MediaQueryDataProvider( + child: DefaultTabController( + length: 4, + child: Scaffold( + appBar: AppBar( + title: Text('Preferences'), + ), + body: SafeArea( + child: ListView( + padding: EdgeInsets.all(16), + children: [ + Text('Maps'), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Storage:'), + SizedBox(width: 8), + Flexible(child: InfoMapStyleSelector()), + ], + ), + ], + ), + ), + ), + ), + ); + } +} + +class InfoMapStyleSelector extends StatefulWidget { + @override + _InfoMapStyleSelectorState createState() => _InfoMapStyleSelectorState(); +} + +class _InfoMapStyleSelectorState extends State { + @override + Widget build(BuildContext context) { + return DropdownButton( + items: EntryMapStyle.values + .map((style) => DropdownMenuItem( + value: style, + child: Text( + style.name, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + )) + .toList(), + value: settings.infoMapStyle, + onChanged: (style) { + settings.infoMapStyle = style; + setState(() {}); + }, + ); + } +} diff --git a/pubspec.lock b/pubspec.lock index 329e3324c..cc51e1ed7 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,13 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + ansicolor: + dependency: transitive + description: + name: ansicolor + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.2" archive: dependency: transitive description: @@ -36,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.0" + cached_network_image: + dependency: transitive + description: + name: cached_network_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0+1" characters: dependency: transitive description: @@ -78,6 +92,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.14.13" + console_log_handler: + dependency: transitive + description: + name: console_log_handler + url: "https://pub.dartlang.org" + source: hosted + version: "1.1.6" convert: dependency: transitive description: @@ -150,6 +171,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_cache_manager: + dependency: transitive + description: + name: flutter_cache_manager + url: "https://pub.dartlang.org" + source: hosted + version: "1.4.1" flutter_ijkplayer: dependency: "direct main" description: @@ -159,6 +187,20 @@ packages: url: "git://github.com/deckerst/flutter_ijkplayer.git" source: git version: "0.3.7" + flutter_image: + dependency: transitive + description: + name: flutter_image + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + url: "https://pub.dartlang.org" + source: hosted + version: "0.10.1+1" flutter_markdown: dependency: "direct main" description: @@ -225,6 +267,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.3" + http: + dependency: transitive + description: + name: http + url: "https://pub.dartlang.org" + source: hosted + version: "0.12.2" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.4" image: dependency: transitive description: @@ -246,6 +302,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.6.2" + latlong: + dependency: "direct main" + description: + name: latlong + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.1" + lists: + dependency: transitive + description: + name: lists + url: "https://pub.dartlang.org" + source: hosted + version: "0.1.6" logging: dependency: transitive description: @@ -274,6 +344,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.8" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" nested: dependency: transitive description: @@ -323,6 +400,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.1.4" + path_provider: + dependency: transitive + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "1.6.11" path_provider_linux: dependency: transitive description: @@ -330,6 +414,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.0.1+2" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "0.0.4+3" path_provider_platform_interface: dependency: transitive description: @@ -409,6 +500,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.2" + positioned_tap_detector: + dependency: transitive + description: + name: positioned_tap_detector + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.3" printing: dependency: "direct main" description: @@ -423,6 +521,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.13" + proj4dart: + dependency: transitive + description: + name: proj4dart + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.5" provider: dependency: "direct main" description: @@ -451,6 +556,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.3" + rxdart: + dependency: transitive + description: + name: rxdart + url: "https://pub.dartlang.org" + source: hosted + version: "0.24.1" screen: dependency: "direct main" description: @@ -575,6 +687,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.2.17" + transparent_image: + dependency: transitive + description: + name: transparent_image + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.0" tuple: dependency: "direct main" description: @@ -589,6 +708,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.2.0" + unicode: + dependency: transitive + description: + name: unicode + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.4" url_launcher: dependency: "direct main" description: @@ -638,6 +764,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.0" + validate: + dependency: transitive + description: + name: validate + url: "https://pub.dartlang.org" + source: hosted + version: "1.7.0" vector_math: dependency: transitive description: @@ -645,6 +778,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.0.8" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.7" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 677b3a265..a8344e157 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -49,6 +49,7 @@ dependencies: # path: ../flutter_ijkplayer git: url: git://github.com/deckerst/flutter_ijkplayer.git + flutter_map: flutter_markdown: flutter_native_timezone: flutter_staggered_animations: @@ -56,6 +57,7 @@ dependencies: geocoder: google_maps_flutter: intl: + latlong: # for flutter_map outline_material_icons: package_info: palette_generator: From 2a7bc663d67e40f982176a39d9b9a2af63e06717 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 9 Aug 2020 22:14:18 +0900 Subject: [PATCH 02/10] simplified border radius calls --- lib/widgets/common/dialog.dart | 4 +--- lib/widgets/common/fx/blurred.dart | 2 +- lib/widgets/common/icons.dart | 4 +--- lib/widgets/common/scroll_thumb.dart | 8 ++------ lib/widgets/fullscreen/info/maps/google_map.dart | 4 +--- lib/widgets/fullscreen/info/maps/leaflet_map.dart | 4 +--- lib/widgets/fullscreen/overlay/video.dart | 4 +--- 7 files changed, 8 insertions(+), 22 deletions(-) diff --git a/lib/widgets/common/dialog.dart b/lib/widgets/common/dialog.dart index 01b919004..2681b9101 100644 --- a/lib/widgets/common/dialog.dart +++ b/lib/widgets/common/dialog.dart @@ -35,9 +35,7 @@ class AvesDialog extends AlertDialog { actions: actions, actionsPadding: EdgeInsets.symmetric(horizontal: 8), shape: RoundedRectangleBorder( - borderRadius: BorderRadius.all( - Radius.circular(24), - ), + borderRadius: BorderRadius.circular(24), ), ); } diff --git a/lib/widgets/common/fx/blurred.dart b/lib/widgets/common/fx/blurred.dart index 04e7e75aa..e21af0e29 100644 --- a/lib/widgets/common/fx/blurred.dart +++ b/lib/widgets/common/fx/blurred.dart @@ -27,7 +27,7 @@ class BlurredRRect extends StatelessWidget { @override Widget build(BuildContext context) { return ClipRRect( - borderRadius: BorderRadius.all(Radius.circular(borderRadius)), + borderRadius: BorderRadius.circular(borderRadius), child: BackdropFilter( filter: ImageFilter.blur(sigmaX: 4, sigmaY: 4), child: child, diff --git a/lib/widgets/common/icons.dart b/lib/widgets/common/icons.dart index e08962239..a1de125b7 100644 --- a/lib/widgets/common/icons.dart +++ b/lib/widgets/common/icons.dart @@ -137,9 +137,7 @@ class OverlayIcon extends StatelessWidget { padding: text != null ? EdgeInsets.only(right: size / 4) : null, decoration: BoxDecoration( color: Color(0xBB000000), - borderRadius: BorderRadius.all( - Radius.circular(size), - ), + borderRadius: BorderRadius.circular(size), ), child: text == null ? iconChild diff --git a/lib/widgets/common/scroll_thumb.dart b/lib/widgets/common/scroll_thumb.dart index c2434f326..de907a8b3 100644 --- a/lib/widgets/common/scroll_thumb.dart +++ b/lib/widgets/common/scroll_thumb.dart @@ -12,9 +12,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({ final scrollThumb = Container( decoration: BoxDecoration( color: Colors.black26, - borderRadius: BorderRadius.all( - Radius.circular(12.0), - ), + borderRadius: BorderRadius.circular(12.0), ), height: height, margin: EdgeInsets.only(right: .5), @@ -24,9 +22,7 @@ ScrollThumbBuilder avesScrollThumbBuilder({ width: 20.0, decoration: BoxDecoration( color: backgroundColor, - borderRadius: BorderRadius.all( - Radius.circular(12.0), - ), + borderRadius: BorderRadius.circular(12.0), ), ), clipper: ArrowClipper(), diff --git a/lib/widgets/fullscreen/info/maps/google_map.dart b/lib/widgets/fullscreen/info/maps/google_map.dart index 39fae754a..fb23ae4af 100644 --- a/lib/widgets/fullscreen/info/maps/google_map.dart +++ b/lib/widgets/fullscreen/info/maps/google_map.dart @@ -48,9 +48,7 @@ class EntryGoogleMapState extends State with AutomaticKeepAliveC // and triggering by mistake a move to the image page above }, child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(16), - ), + borderRadius: BorderRadius.circular(16), child: Container( color: Colors.white70, height: 200, diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart index de913348f..be881ce9d 100644 --- a/lib/widgets/fullscreen/info/maps/leaflet_map.dart +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -54,9 +54,7 @@ class EntryLeafletMapState extends State with AutomaticKeepAliv // and triggering by mistake a move to the image page above }, child: ClipRRect( - borderRadius: BorderRadius.all( - Radius.circular(16), - ), + borderRadius: BorderRadius.circular(16), child: Container( color: Colors.white70, height: 200, diff --git a/lib/widgets/fullscreen/overlay/video.dart b/lib/widgets/fullscreen/overlay/video.dart index 7b7e93405..e8baef6ef 100644 --- a/lib/widgets/fullscreen/overlay/video.dart +++ b/lib/widgets/fullscreen/overlay/video.dart @@ -182,9 +182,7 @@ class VideoControlOverlayState extends State with SingleTic decoration: BoxDecoration( color: FullscreenOverlay.backgroundColor, border: FullscreenOverlay.buildBorder(context), - borderRadius: BorderRadius.all( - Radius.circular(progressBarBorderRadius), - ), + borderRadius: BorderRadius.circular(progressBarBorderRadius), ), child: Column( key: _progressBarKey, From 34a888fa3f9b79a49ada5b9a215fdccab1262347 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Sun, 9 Aug 2020 22:17:58 +0900 Subject: [PATCH 03/10] info: improved leaflet map scale display --- .../fullscreen/info/maps/leaflet_map.dart | 16 +-- .../fullscreen/info/maps/scale_layer.dart | 129 +++++++++--------- 2 files changed, 65 insertions(+), 80 deletions(-) diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart index be881ce9d..8c356cf90 100644 --- a/lib/widgets/fullscreen/info/maps/leaflet_map.dart +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -67,15 +67,7 @@ class EntryLeafletMapState extends State with AutomaticKeepAliv children: [ _buildMapLayer(), ScaleLayerWidget( - options: ScaleLayerOptions( - lineColor: accentColor, - lineWidth: 2, - textStyle: TextStyle( - color: accentColor, - fontSize: 12, - ), - padding: EdgeInsets.all(8), - ), + options: ScaleLayerOptions(), ), MarkerLayerWidget( options: MarkerLayerOptions( @@ -171,8 +163,6 @@ class OSMHotLayer extends StatelessWidget { return TileLayerWidget( options: TileLayerOptions( // attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France' - minZoom: 1, - maxZoom: 19, urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], retinaMode: MediaQuery.of(context).devicePixelRatio > 1, @@ -187,8 +177,6 @@ class StamenTonerLayer extends StatelessWidget { return TileLayerWidget( options: TileLayerOptions( // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', - minZoom: 1, - maxZoom: 20, urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', subdomains: ['a', 'b', 'c', 'd'], retinaMode: MediaQuery.of(context).devicePixelRatio > 1, @@ -203,8 +191,6 @@ class StamenWatercolorLayer extends StatelessWidget { return TileLayerWidget( options: TileLayerOptions( // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', - minZoom: 1, - maxZoom: 16, urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', subdomains: ['a', 'b', 'c', 'd'], retinaMode: MediaQuery.of(context).devicePixelRatio > 1, diff --git a/lib/widgets/fullscreen/info/maps/scale_layer.dart b/lib/widgets/fullscreen/info/maps/scale_layer.dart index 06cf170d2..70bf1ad4a 100644 --- a/lib/widgets/fullscreen/info/maps/scale_layer.dart +++ b/lib/widgets/fullscreen/info/maps/scale_layer.dart @@ -1,6 +1,6 @@ import 'dart:math'; -import 'dart:ui' as ui; +import 'package:aves/labs/outlined_text.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:flutter_map/plugin_api.dart'; @@ -8,19 +8,20 @@ import 'package:flutter_map/plugin_api.dart'; import 'scalebar_utils.dart' as util; class ScaleLayerOptions extends LayerOptions { - TextStyle textStyle; - Color lineColor; - double lineWidth; - final EdgeInsets padding; + final Widget Function(double width, String distance) builder; ScaleLayerOptions({ Key key, - this.textStyle, - this.lineColor = Colors.white, - this.lineWidth = 2, - this.padding, + this.builder = defaultBuilder, rebuild, }) : super(key: key, rebuild: rebuild); + + static Widget defaultBuilder(double width, String distance) { + return ScaleBar( + distance: distance, + width: width, + ); + } } class ScaleLayerWidget extends StatelessWidget { @@ -72,70 +73,68 @@ class ScaleLayer extends StatelessWidget { return StreamBuilder( stream: stream, builder: (context, snapshot) { - var zoom = map.zoom; - var distance = scale[max(0, min(20, zoom.round() + 2))].toDouble(); - var center = map.center; - var start = map.project(center); - var targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance); - var end = map.project(targetPoint); - var displayDistance = distance > 999 ? '${(distance / 1000).toStringAsFixed(0)} km' : '${distance.toStringAsFixed(0)} m'; - double width = (end.x - start.x); + final center = map.center; + final latitude = center.latitude.abs(); + final level = map.zoom.round() + (latitude > 80 ? 4 : latitude > 60 ? 3 : 2); + final distance = scale[max(0, min(20, level))].toDouble(); + final start = map.project(center); + final targetPoint = util.calculateEndingGlobalCoordinates(center, 90, distance); + final end = map.project(targetPoint); + final displayDistance = distance > 999 ? '${(distance / 1000).toStringAsFixed(0)} km' : '${distance.toStringAsFixed(0)} m'; + final double width = (end.x - start.x); - return CustomPaint( - painter: ScalePainter( - width, - displayDistance, - lineColor: scaleLayerOpts.lineColor, - lineWidth: scaleLayerOpts.lineWidth, - padding: scaleLayerOpts.padding, - textStyle: scaleLayerOpts.textStyle, - ), - ); + return scaleLayerOpts.builder(width, displayDistance); }, ); } } -class ScalePainter extends CustomPainter { - ScalePainter(this.width, this.text, {this.padding, this.textStyle, this.lineWidth, this.lineColor}); - +class ScaleBar extends StatelessWidget { + final String distance; final double width; - final EdgeInsets padding; - final String text; - TextStyle textStyle; - double lineWidth; - Color lineColor; + + static const Color fillColor = Colors.black; + static const Color outlineColor = Colors.white; + static const double outlineWidth = .5; + static const double barThickness = 1; + + const ScaleBar({ + @required this.distance, + @required this.width, + }); @override - void paint(ui.Canvas canvas, ui.Size size) { - final paint = Paint() - ..color = lineColor - ..strokeCap = StrokeCap.square - ..strokeWidth = lineWidth; - - var sizeForStartEnd = 4; - var paddingLeft = padding == null ? 0 : padding.left + sizeForStartEnd / 2; - var paddingTop = padding == null ? 0 : padding.top; - - var textSpan = TextSpan(style: textStyle, text: text); - var textPainter = TextPainter(text: textSpan, textDirection: TextDirection.ltr)..layout(); - textPainter.paint(canvas, Offset(width / 2 - textPainter.width / 2 + paddingLeft, paddingTop)); - paddingTop += textPainter.height; - var p1 = Offset(paddingLeft, sizeForStartEnd + paddingTop); - var p2 = Offset(paddingLeft + width, sizeForStartEnd + paddingTop); - // draw start line - canvas.drawLine(Offset(paddingLeft, paddingTop), Offset(paddingLeft, sizeForStartEnd + paddingTop), paint); - // draw middle line - var middleX = width / 2 + paddingLeft - lineWidth / 2; - canvas.drawLine(Offset(middleX, paddingTop + sizeForStartEnd / 2), Offset(middleX, sizeForStartEnd + paddingTop), paint); - // draw end line - canvas.drawLine(Offset(width + paddingLeft, paddingTop), Offset(width + paddingLeft, sizeForStartEnd + paddingTop), paint); - // draw bottom line - canvas.drawLine(p1, p2, paint); - } - - @override - bool shouldRepaint(CustomPainter oldDelegate) { - return true; + Widget build(BuildContext context) { + return Container( + alignment: AlignmentDirectional.bottomStart, + padding: EdgeInsets.symmetric(vertical: 8, horizontal: 16), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + OutlinedText( + text: distance, + style: TextStyle( + color: fillColor, + fontSize: 11, + ), + outlineWidth: outlineWidth * 2, + outlineColor: outlineColor, + ), + Container( + height: barThickness + outlineWidth * 2, + width: width, + decoration: BoxDecoration( + color: fillColor, + border: Border.all( + color: outlineColor, + width: outlineWidth, + ), + borderRadius: BorderRadius.circular(8), + ), + ), + ], + ), + ); } } From 1d77ec9e5e071716394b5ae7c20d03312f0f8dbb Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 17 Aug 2020 08:58:22 +0900 Subject: [PATCH 04/10] selection: toggle on long press even in selection mode --- lib/widgets/album/grid/list_sliver.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/widgets/album/grid/list_sliver.dart b/lib/widgets/album/grid/list_sliver.dart index 97fff2258..dc8845a33 100644 --- a/lib/widgets/album/grid/list_sliver.dart +++ b/lib/widgets/album/grid/list_sliver.dart @@ -65,7 +65,7 @@ class GridThumbnail extends StatelessWidget { } }, onLongPress: () { - if (AvesApp.mode == AppMode.main && collection.isBrowsing) { + if (AvesApp.mode == AppMode.main) { collection.toggleSelection(entry); } }, From 6bc416b6b669ef002e91a9c094ed85783413edd8 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 17 Aug 2020 09:45:22 +0900 Subject: [PATCH 05/10] settings: added option to show album list on launch --- lib/model/settings.dart | 40 +++++++++--- lib/widgets/app_drawer.dart | 103 +++--------------------------- lib/widgets/filter_grid_page.dart | 82 +++++++++++++++++++++++- lib/widgets/home_page.dart | 17 +++-- lib/widgets/settings_page.dart | 52 +++++++++++++-- 5 files changed, 177 insertions(+), 117 deletions(-) diff --git a/lib/model/settings.dart b/lib/model/settings.dart index 801340b09..f634b4279 100644 --- a/lib/model/settings.dart +++ b/lib/model/settings.dart @@ -16,13 +16,14 @@ class Settings { Settings._private(); // preferences + static const catalogTimeZoneKey = 'catalog_time_zone'; static const collectionGroupFactorKey = 'collection_group_factor'; static const collectionSortFactorKey = 'collection_sort_factor'; static const collectionTileExtentKey = 'collection_tile_extent'; + static const hasAcceptedTermsKey = 'has_accepted_terms'; static const infoMapStyleKey = 'info_map_style'; static const infoMapZoomKey = 'info_map_zoom'; - static const catalogTimeZoneKey = 'catalog_time_zone'; - static const hasAcceptedTermsKey = 'has_accepted_terms'; + static const launchPageKey = 'launch_page'; Future init() async { _prefs = await SharedPreferences.getInstance(); @@ -52,14 +53,6 @@ class Settings { } } - EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); - - set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); - - double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; - - set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); - String get catalogTimeZone => _prefs.getString(catalogTimeZoneKey) ?? ''; set catalogTimeZone(String newValue) => setAndNotify(catalogTimeZoneKey, newValue); @@ -76,10 +69,22 @@ class Settings { set collectionTileExtent(double newValue) => setAndNotify(collectionTileExtentKey, newValue); + EntryMapStyle get infoMapStyle => getEnumOrDefault(infoMapStyleKey, EntryMapStyle.stamenWatercolor, EntryMapStyle.values); + + set infoMapStyle(EntryMapStyle newValue) => setAndNotify(infoMapStyleKey, newValue.toString()); + + double get infoMapZoom => _prefs.getDouble(infoMapZoomKey) ?? 12; + + set infoMapZoom(double newValue) => setAndNotify(infoMapZoomKey, newValue); + bool get hasAcceptedTerms => getBoolOrDefault(hasAcceptedTermsKey, false); set hasAcceptedTerms(bool newValue) => setAndNotify(hasAcceptedTermsKey, newValue); + LaunchPage get launchPage => getEnumOrDefault(launchPageKey, LaunchPage.collection, LaunchPage.values); + + set launchPage(LaunchPage newValue) => setAndNotify(launchPageKey, newValue.toString()); + // convenience methods // ignore: avoid_positional_boolean_parameters @@ -124,3 +129,18 @@ class Settings { } } } + +enum LaunchPage { collection, albums } + +extension ExtraLaunchPage on LaunchPage { + String get name { + switch (this) { + case LaunchPage.collection: + return 'All Media'; + case LaunchPage.albums: + return 'Albums'; + default: + return toString(); + } + } +} diff --git a/lib/widgets/app_drawer.dart b/lib/widgets/app_drawer.dart index c0ee4e7f3..29bde3213 100644 --- a/lib/widgets/app_drawer.dart +++ b/lib/widgets/app_drawer.dart @@ -3,9 +3,7 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/favourite.dart'; import 'package:aves/model/filters/filters.dart'; -import 'package:aves/model/filters/location.dart'; import 'package:aves/model/filters/mime.dart'; -import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/mime_types.dart'; import 'package:aves/model/settings.dart'; import 'package:aves/model/source/album.dart'; @@ -16,7 +14,6 @@ import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/about/about_page.dart'; import 'package:aves/widgets/album/collection_page.dart'; -import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/common/aves_logo.dart'; import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/debug_page.dart'; @@ -100,7 +97,7 @@ class _AppDrawerState extends State { child: ListTile( leading: Icon(AIcons.settings), title: Text('Preferences'), - onTap: () => _goToSettings(context), + onTap: () => _goTo((_) => SettingsPage()), ), ); final aboutEntry = SafeArea( @@ -109,7 +106,7 @@ class _AppDrawerState extends State { child: ListTile( leading: Icon(AIcons.info), title: Text('About'), - onTap: () => _goToAbout(context), + onTap: () => _goTo((_) => AboutPage()), ), ); @@ -134,7 +131,7 @@ class _AppDrawerState extends State { child: ListTile( leading: Icon(AIcons.debug), title: Text('Debug'), - onTap: () => _goToDebug(context), + onTap: () => _goTo((_) => DebugPage(source: source)), ), ), ], @@ -215,7 +212,7 @@ class _AppDrawerState extends State { ), ); }), - onTap: () => _goToAlbums(context), + onTap: () => _goTo((_) => AlbumListPage(source: source)), ), ); } @@ -237,7 +234,7 @@ class _AppDrawerState extends State { ), ); }), - onTap: () => _goToCountries(context), + onTap: () => _goTo((_) => CountryListPage(source: source)), ), ); } @@ -259,98 +256,14 @@ class _AppDrawerState extends State { ), ); }), - onTap: () => _goToTags(context), + onTap: () => _goTo((_) => TagListPage(source: source)), ), ); } - void _goToAlbums(BuildContext context) { + void _goTo(WidgetBuilder builder) { Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FilterNavigationPage( - source: source, - title: 'Albums', - filterEntries: source.getAlbumEntries(), - filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), - emptyBuilder: () => EmptyContent( - icon: AIcons.album, - text: 'No albums', - ), - ), - ), - ); - } - - void _goToCountries(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FilterNavigationPage( - source: source, - title: 'Countries', - filterEntries: source.getCountryEntries(), - filterBuilder: (s) => LocationFilter(LocationLevel.country, s), - emptyBuilder: () => EmptyContent( - icon: AIcons.location, - text: 'No countries', - ), - ), - ), - ); - } - - void _goToTags(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => FilterNavigationPage( - source: source, - title: 'Tags', - filterEntries: source.getTagEntries(), - filterBuilder: (s) => TagFilter(s), - emptyBuilder: () => EmptyContent( - icon: AIcons.tag, - text: 'No tags', - ), - ), - ), - ); - } - - void _goToSettings(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => SettingsPage(), - ), - ); - } - - void _goToAbout(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => AboutPage(), - ), - ); - } - - void _goToDebug(BuildContext context) { - Navigator.pop(context); - Navigator.push( - context, - MaterialPageRoute( - builder: (context) => DebugPage( - source: source, - ), - ), - ); + Navigator.push(context, MaterialPageRoute(builder: builder)); } } diff --git a/lib/widgets/filter_grid_page.dart b/lib/widgets/filter_grid_page.dart index 437f1e9c1..2862d2043 100644 --- a/lib/widgets/filter_grid_page.dart +++ b/lib/widgets/filter_grid_page.dart @@ -2,13 +2,19 @@ import 'dart:ui'; import 'package:aves/model/filters/album.dart'; import 'package:aves/model/filters/filters.dart'; +import 'package:aves/model/filters/location.dart'; +import 'package:aves/model/filters/tag.dart'; import 'package:aves/model/image_entry.dart'; import 'package:aves/model/settings.dart'; +import 'package:aves/model/source/album.dart'; import 'package:aves/model/source/collection_lens.dart'; import 'package:aves/model/source/collection_source.dart'; +import 'package:aves/model/source/location.dart'; +import 'package:aves/model/source/tag.dart'; import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/utils/durations.dart'; import 'package:aves/widgets/album/collection_page.dart'; +import 'package:aves/widgets/album/empty.dart'; import 'package:aves/widgets/album/thumbnail/raster.dart'; import 'package:aves/widgets/album/thumbnail/vector.dart'; import 'package:aves/widgets/app_drawer.dart'; @@ -20,6 +26,75 @@ import 'package:flutter/material.dart'; import 'package:flutter_staggered_animations/flutter_staggered_animations.dart'; import 'package:provider/provider.dart'; +class AlbumListPage extends StatelessWidget { + final CollectionSource source; + + const AlbumListPage({@required this.source}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Albums', + filterEntries: source.getAlbumEntries(), + filterBuilder: (s) => AlbumFilter(s, source.getUniqueAlbumName(s)), + emptyBuilder: () => EmptyContent( + icon: AIcons.album, + text: 'No albums', + ), + ), + ); + } +} + +class CountryListPage extends StatelessWidget { + final CollectionSource source; + + const CountryListPage({@required this.source}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Countries', + filterEntries: source.getCountryEntries(), + filterBuilder: (s) => LocationFilter(LocationLevel.country, s), + emptyBuilder: () => EmptyContent( + icon: AIcons.location, + text: 'No countries', + ), + ), + ); + } +} + +class TagListPage extends StatelessWidget { + final CollectionSource source; + + const TagListPage({@required this.source}); + + @override + Widget build(BuildContext context) { + return StreamBuilder( + stream: source.eventBus.on(), + builder: (context, snapshot) => FilterNavigationPage( + source: source, + title: 'Tags', + filterEntries: source.getTagEntries(), + filterBuilder: (s) => TagFilter(s), + emptyBuilder: () => EmptyContent( + icon: AIcons.tag, + text: 'No tags', + ), + ), + ); + } +} + class FilterNavigationPage extends StatelessWidget { final CollectionSource source; final String title; @@ -48,7 +123,12 @@ class FilterNavigationPage extends StatelessWidget { ), filterEntries: filterEntries, filterBuilder: filterBuilder, - emptyBuilder: emptyBuilder, + emptyBuilder: () => ValueListenableBuilder( + valueListenable: source.stateNotifier, + builder: (context, sourceState, child) { + return sourceState != SourceState.loading && emptyBuilder != null ? emptyBuilder() : SizedBox.shrink(); + }, + ), onPressed: (filter) => Navigator.pushAndRemoveUntil( context, MaterialPageRoute( diff --git a/lib/widgets/home_page.dart b/lib/widgets/home_page.dart index 6365e0c1e..b211ddec5 100644 --- a/lib/widgets/home_page.dart +++ b/lib/widgets/home_page.dart @@ -8,6 +8,7 @@ import 'package:aves/utils/android_file_utils.dart'; import 'package:aves/widgets/album/collection_page.dart'; import 'package:aves/widgets/common/data_providers/media_store_collection_provider.dart'; import 'package:aves/widgets/common/icons.dart'; +import 'package:aves/widgets/filter_grid_page.dart'; import 'package:aves/widgets/fullscreen/fullscreen_page.dart'; import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; @@ -101,11 +102,17 @@ class _HomePageState extends State { return SingleFullscreenPage(entry: _viewerEntry); } if (_mediaStore != null) { - return CollectionPage(CollectionLens( - source: _mediaStore, - groupFactor: settings.collectionGroupFactor, - sortFactor: settings.collectionSortFactor, - )); + switch(settings.launchPage) { + case LaunchPage.albums: + return AlbumListPage(source: _mediaStore); + break; + case LaunchPage.collection: + return CollectionPage(CollectionLens( + source: _mediaStore, + groupFactor: settings.collectionGroupFactor, + sortFactor: settings.collectionSortFactor, + )); + } } return SizedBox.shrink(); }); diff --git a/lib/widgets/settings_page.dart b/lib/widgets/settings_page.dart index f12b8cd1f..8a0b2af4f 100644 --- a/lib/widgets/settings_page.dart +++ b/lib/widgets/settings_page.dart @@ -1,4 +1,5 @@ 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'; @@ -17,7 +18,17 @@ class SettingsPage extends StatelessWidget { child: ListView( padding: EdgeInsets.all(16), children: [ - Text('Maps'), + Text('General', style: Constants.titleTextStyle), + Row( + mainAxisSize: MainAxisSize.min, + children: [ + Text('Launch Page:'), + SizedBox(width: 8), + Flexible(child: LaunchPageSelector()), + ], + ), + SizedBox(height: 16), + Text('Maps', style: Constants.titleTextStyle), Row( mainAxisSize: MainAxisSize.min, children: [ @@ -45,10 +56,10 @@ class _InfoMapStyleSelectorState extends State { Widget build(BuildContext context) { return DropdownButton( items: EntryMapStyle.values - .map((style) => DropdownMenuItem( - value: style, + .map((selected) => DropdownMenuItem( + value: selected, child: Text( - style.name, + selected.name, softWrap: false, overflow: TextOverflow.fade, maxLines: 1, @@ -56,8 +67,37 @@ class _InfoMapStyleSelectorState extends State { )) .toList(), value: settings.infoMapStyle, - onChanged: (style) { - settings.infoMapStyle = style; + onChanged: (selected) { + settings.infoMapStyle = selected; + setState(() {}); + }, + ); + } +} + +class LaunchPageSelector extends StatefulWidget { + @override + _LaunchPageSelectorState createState() => _LaunchPageSelectorState(); +} + +class _LaunchPageSelectorState extends State { + @override + Widget build(BuildContext context) { + return DropdownButton( + items: LaunchPage.values + .map((selected) => DropdownMenuItem( + value: selected, + child: Text( + selected.name, + softWrap: false, + overflow: TextOverflow.fade, + maxLines: 1, + ), + )) + .toList(), + value: settings.launchPage, + onChanged: (selected) { + settings.launchPage = selected; setState(() {}); }, ); From f98400179bb8f8ad451858ba45415e5f148caa5f Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 17 Aug 2020 10:40:53 +0900 Subject: [PATCH 06/10] info: added map credits --- .../fullscreen/info/maps/leaflet_map.dart | 176 +++++++++++------- 1 file changed, 107 insertions(+), 69 deletions(-) diff --git a/lib/widgets/fullscreen/info/maps/leaflet_map.dart b/lib/widgets/fullscreen/info/maps/leaflet_map.dart index 8c356cf90..464a46607 100644 --- a/lib/widgets/fullscreen/info/maps/leaflet_map.dart +++ b/lib/widgets/fullscreen/info/maps/leaflet_map.dart @@ -4,8 +4,10 @@ import 'package:aves/widgets/common/icons.dart'; import 'package:aves/widgets/fullscreen/info/maps/scale_layer.dart'; import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; +import 'package:flutter_markdown/flutter_markdown.dart'; import 'package:latlong/latlong.dart'; import 'package:tuple/tuple.dart'; +import 'package:url_launcher/url_launcher.dart'; import '../location_section.dart'; @@ -45,79 +47,85 @@ class EntryLeafletMapState extends State with AutomaticKeepAliv Widget build(BuildContext context) { super.build(context); final accentColor = Theme.of(context).accentColor; - return Row( + return Column( + crossAxisAlignment: CrossAxisAlignment.start, 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), - ), - ], + Row( + 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, ), - ], - mapController: _mapController, + ), ), ), ), - ), + 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...', + ), + ]), + ) + ], ), - 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(), ], ); } @@ -135,6 +143,39 @@ 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/)'; + 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'; + default: + return null; + } + } + void _zoomBy(double amount) { final endZoom = (settings.infoMapZoom + amount).clamp(1.0, 16.0); settings.infoMapZoom = endZoom; @@ -162,7 +203,6 @@ class OSMHotLayer extends StatelessWidget { Widget build(BuildContext context) { return TileLayerWidget( options: TileLayerOptions( - // attribution: '© OpenStreetMap contributors, Tiles style by Humanitarian OpenStreetMap Team hosted by OpenStreetMap France' urlTemplate: 'https://{s}.tile.openstreetmap.fr/hot/{z}/{x}/{y}.png', subdomains: ['a', 'b', 'c'], retinaMode: MediaQuery.of(context).devicePixelRatio > 1, @@ -176,7 +216,6 @@ class StamenTonerLayer extends StatelessWidget { Widget build(BuildContext context) { return TileLayerWidget( options: TileLayerOptions( - // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/toner-lite/{z}/{x}/{y}{r}.png', subdomains: ['a', 'b', 'c', 'd'], retinaMode: MediaQuery.of(context).devicePixelRatio > 1, @@ -190,7 +229,6 @@ class StamenWatercolorLayer extends StatelessWidget { Widget build(BuildContext context) { return TileLayerWidget( options: TileLayerOptions( - // attribution: 'Map tiles by Stamen Design, CC BY 3.0 — Map data © OpenStreetMap contributors', urlTemplate: 'https://stamen-tiles-{s}.a.ssl.fastly.net/watercolor/{z}/{x}/{y}.jpg', subdomains: ['a', 'b', 'c', 'd'], retinaMode: MediaQuery.of(context).devicePixelRatio > 1, From 2528370f73921eb3fe6b4a06f3b9e24b8aff8de4 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 17 Aug 2020 10:43:19 +0900 Subject: [PATCH 07/10] minor: comment --- lib/widgets/fullscreen/info/location_section.dart | 1 + 1 file changed, 1 insertion(+) diff --git a/lib/widgets/fullscreen/info/location_section.dart b/lib/widgets/fullscreen/info/location_section.dart index 380bdbc59..4dac6a6ba 100644 --- a/lib/widgets/fullscreen/info/location_section.dart +++ b/lib/widgets/fullscreen/info/location_section.dart @@ -138,6 +138,7 @@ class _LocationSectionState extends State { void _handleChange() => setState(() {}); } +// browse providers at https://leaflet-extras.github.io/leaflet-providers/preview/ enum EntryMapStyle { google, osmHot, stamenToner, stamenWatercolor } extension ExtraEntryMapStyle on EntryMapStyle { From 5bca875f83982403c7577df7c1459ff35b4935d6 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 17 Aug 2020 12:40:49 +0900 Subject: [PATCH 08/10] info: use overlay buttons and added map style button --- .../action_delegates/map_style_dialog.dart | 51 +++++ lib/widgets/common/icons.dart | 1 + .../fullscreen/info/location_section.dart | 34 ++-- lib/widgets/fullscreen/info/maps/buttons.dart | 112 +++++++++++ .../fullscreen/info/maps/google_map.dart | 111 +++++------ .../fullscreen/info/maps/leaflet_map.dart | 176 ++++++++---------- lib/widgets/home_page.dart | 2 +- lib/widgets/settings_page.dart | 40 ---- 8 files changed, 312 insertions(+), 215 deletions(-) create mode 100644 lib/widgets/common/action_delegates/map_style_dialog.dart create mode 100644 lib/widgets/fullscreen/info/maps/buttons.dart 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(); From b8f38a5b510cb3069b8f7d83f08ef09c928dbdb9 Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 17 Aug 2020 12:45:06 +0900 Subject: [PATCH 09/10] updated packages, call dispose on google map --- lib/widgets/fullscreen/info/maps/google_map.dart | 6 ++++++ pubspec.lock | 10 +++++----- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/lib/widgets/fullscreen/info/maps/google_map.dart b/lib/widgets/fullscreen/info/maps/google_map.dart index 4b59edb35..9971b95df 100644 --- a/lib/widgets/fullscreen/info/maps/google_map.dart +++ b/lib/widgets/fullscreen/info/maps/google_map.dart @@ -34,6 +34,12 @@ class EntryGoogleMapState extends State with AutomaticKeepAliveC } } + @override + void dispose() { + super.dispose(); + _controller?.dispose(); + } + @override Widget build(BuildContext context) { super.build(context); diff --git a/pubspec.lock b/pubspec.lock index cc51e1ed7..4866bb809 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -259,14 +259,14 @@ packages: name: google_maps_flutter url: "https://pub.dartlang.org" source: hosted - version: "0.5.29" + version: "0.5.30" google_maps_flutter_platform_interface: dependency: transitive description: name: google_maps_flutter_platform_interface url: "https://pub.dartlang.org" source: hosted - version: "1.0.3" + version: "1.0.4" http: dependency: transitive description: @@ -434,7 +434,7 @@ packages: name: pdf url: "https://pub.dartlang.org" source: hosted - version: "1.10.0" + version: "1.10.1" pedantic: dependency: "direct main" description: @@ -513,7 +513,7 @@ packages: name: printing url: "https://pub.dartlang.org" source: hosted - version: "3.5.0" + version: "3.6.0" process: dependency: transitive description: @@ -749,7 +749,7 @@ packages: name: url_launcher_web url: "https://pub.dartlang.org" source: hosted - version: "0.1.2" + version: "0.1.2+1" utf: dependency: transitive description: From e5ef2011762834ecb11786f5c35d9e10d5aefc4b Mon Sep 17 00:00:00 2001 From: Thibault Deckers Date: Mon, 17 Aug 2020 12:48:25 +0900 Subject: [PATCH 10/10] version bump --- pubspec.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pubspec.yaml b/pubspec.yaml index a8344e157..5626daa28 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -11,7 +11,7 @@ description: A new Flutter application. # In iOS, build-name is used as CFBundleShortVersionString while build-number used as CFBundleVersion. # Read more about iOS versioning at # https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html -version: 1.1.4+16 +version: 1.1.5+17 # video_player (as of v0.10.8+2, backed by ExoPlayer): # - does not support content URIs (by default, but trivial by fork)