diff --git a/assets/2.0x/gps_map_pin.png b/assets/2.0x/gps_map_pin.png new file mode 100644 index 00000000..a76280a7 Binary files /dev/null and b/assets/2.0x/gps_map_pin.png differ diff --git a/assets/3.0x/gps_map_pin.png b/assets/3.0x/gps_map_pin.png new file mode 100644 index 00000000..6a1eceab Binary files /dev/null and b/assets/3.0x/gps_map_pin.png differ diff --git a/assets/gps_map_pin.png b/assets/gps_map_pin.png new file mode 100644 index 00000000..0920af14 Binary files /dev/null and b/assets/gps_map_pin.png differ diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index d8d754a2..b63ef9bc 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -352,6 +352,7 @@ "settingsScreenBrightnessDescription": "Override system brightness level", "settingsForceRotationTitle": "Ignore rotation lock", "settingsForceRotationDescription": "Rotate the screen even when auto rotation is disabled", + "settingsMapProviderTitle": "Map provider", "settingsAlbumTitle": "Album", "settingsAlbumDescription": "Customize albums", "settingsAlbumPageTitle": "Album settings", diff --git a/lib/l10n/untranslated-messages.txt b/lib/l10n/untranslated-messages.txt index 8e1232fa..a873507b 100644 --- a/lib/l10n/untranslated-messages.txt +++ b/lib/l10n/untranslated-messages.txt @@ -8,6 +8,7 @@ "settingsShareFolderDialogDescription", "settingsShareFolderPickerDescription", "settingsServerAppSectionTitle", + "settingsMapProviderTitle", "settingsAlbumTitle", "settingsAlbumDescription", "settingsAlbumPageTitle", @@ -59,6 +60,7 @@ "settingsShareFolderDialogDescription", "settingsShareFolderPickerDescription", "settingsServerAppSectionTitle", + "settingsMapProviderTitle", "settingsAlbumTitle", "settingsAlbumDescription", "settingsAlbumPageTitle", @@ -132,6 +134,7 @@ "settingsScreenBrightnessDescription", "settingsForceRotationTitle", "settingsForceRotationDescription", + "settingsMapProviderTitle", "settingsAlbumTitle", "settingsAlbumDescription", "settingsAlbumPageTitle", @@ -235,6 +238,10 @@ "defaultButtonLabel" ], + "es": [ + "settingsMapProviderTitle" + ], + "fr": [ "collectionsTooltip", "settingsAccountTitle", @@ -252,6 +259,7 @@ "settingsScreenBrightnessDescription", "settingsForceRotationTitle", "settingsForceRotationDescription", + "settingsMapProviderTitle", "settingsAlbumTitle", "settingsAlbumDescription", "settingsAlbumPageTitle", @@ -344,6 +352,7 @@ "settingsShareFolderDialogDescription", "settingsShareFolderPickerDescription", "settingsServerAppSectionTitle", + "settingsMapProviderTitle", "settingsAlbumTitle", "settingsAlbumDescription", "settingsAlbumPageTitle", diff --git a/lib/mobile/gps_map.dart b/lib/mobile/google_gps_map.dart similarity index 90% rename from lib/mobile/gps_map.dart rename to lib/mobile/google_gps_map.dart index 5415f832..9809111b 100644 --- a/lib/mobile/gps_map.dart +++ b/lib/mobile/google_gps_map.dart @@ -2,8 +2,8 @@ import 'package:flutter/widgets.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; import 'package:tuple/tuple.dart'; -class GpsMap extends StatelessWidget { - const GpsMap({ +class GoogleGpsMap extends StatelessWidget { + const GoogleGpsMap({ Key? key, required this.center, required this.zoom, @@ -41,7 +41,6 @@ class GpsMap extends StatelessWidget { ); } - /// A pair of latitude and longitude coordinates, stored as degrees final Tuple2 center; final double zoom; final VoidCallback? onTap; diff --git a/lib/mobile/platform.dart b/lib/mobile/platform.dart index 394ce6a2..29cf634f 100644 --- a/lib/mobile/platform.dart +++ b/lib/mobile/platform.dart @@ -1,5 +1,5 @@ export 'db_util.dart'; export 'download.dart'; export 'file_saver.dart'; -export 'gps_map.dart'; +export 'google_gps_map.dart'; export 'universal_storage.dart'; diff --git a/lib/pref.dart b/lib/pref.dart index 2a2b8780..abe63b6e 100644 --- a/lib/pref.dart +++ b/lib/pref.dart @@ -133,6 +133,11 @@ class Pref { Future setNewSharedAlbum(bool value) => provider.setBool(PrefKey.newSharedAlbum, value); + int? getGpsMapProvider() => provider.getInt(PrefKey.gpsMapProvider); + int getGpsMapProviderOr(int def) => getGpsMapProvider() ?? def; + Future setGpsMapProvider(int value) => + provider.setInt(PrefKey.gpsMapProvider, value); + bool? isLabEnableSharedAlbum() => provider.getBool(PrefKey.labEnableSharedAlbum); bool isLabEnableSharedAlbumOr(bool def) => isLabEnableSharedAlbum() ?? def; @@ -308,6 +313,7 @@ enum PrefKey { isSlideshowShuffle, isSlideshowRepeat, isAlbumBrowserShowDate, + gpsMapProvider, } extension on PrefKey { @@ -353,6 +359,8 @@ extension on PrefKey { return "isSlideshowRepeat"; case PrefKey.isAlbumBrowserShowDate: return "isAlbumBrowserShowDate"; + case PrefKey.gpsMapProvider: + return "gpsMapProvider"; } } } diff --git a/lib/web/gps_map.dart b/lib/web/google_gps_map.dart similarity index 78% rename from lib/web/gps_map.dart rename to lib/web/google_gps_map.dart index 4b4fb0bb..5248dc6e 100644 --- a/lib/web/gps_map.dart +++ b/lib/web/google_gps_map.dart @@ -6,8 +6,8 @@ import 'package:/nc_photos/mobile/ui_hack.dart' if (dart.library.html) 'dart:ui' import 'package:flutter/widgets.dart'; import 'package:tuple/tuple.dart'; -class GpsMap extends StatefulWidget { - const GpsMap({ +class GoogleGpsMap extends StatefulWidget { + const GoogleGpsMap({ Key? key, required this.center, required this.zoom, @@ -15,15 +15,14 @@ class GpsMap extends StatefulWidget { }) : super(key: key); @override - createState() => _GpsMapState(); + createState() => _GoogleGpsMapState(); - /// A pair of latitude and longitude coordinates, stored as degrees final Tuple2 center; final double zoom; final void Function()? onTap; } -class _GpsMapState extends State { +class _GoogleGpsMapState extends State { @override initState() { super.initState(); @@ -45,5 +44,5 @@ class _GpsMapState extends State { static const _apiKey = ""; String get viewType => - "mapIframe(${widget.center.item1},${widget.center.item2})"; + "googleMapIframe(${widget.center.item1},${widget.center.item2})"; } diff --git a/lib/web/platform.dart b/lib/web/platform.dart index 394ce6a2..29cf634f 100644 --- a/lib/web/platform.dart +++ b/lib/web/platform.dart @@ -1,5 +1,5 @@ export 'db_util.dart'; export 'download.dart'; export 'file_saver.dart'; -export 'gps_map.dart'; +export 'google_gps_map.dart'; export 'universal_storage.dart'; diff --git a/lib/widget/gps_map.dart b/lib/widget/gps_map.dart new file mode 100644 index 00000000..1bfc570d --- /dev/null +++ b/lib/widget/gps_map.dart @@ -0,0 +1,124 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:flutter_map/flutter_map.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:nc_photos/mobile/platform.dart' + if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; +import 'package:nc_photos/pref.dart'; +import 'package:tuple/tuple.dart'; +import 'package:url_launcher/url_launcher.dart'; + +enum GpsMapProvider { + // the order must not be changed + google, + osm, +} + +extension GpsMapProviderExtension on GpsMapProvider { + String toUserString() { + switch (this) { + case GpsMapProvider.google: + return "Google Maps"; + + case GpsMapProvider.osm: + return "OpenStreetMap"; + } + } +} + +class GpsMap extends StatelessWidget { + const GpsMap({ + Key? key, + required this.center, + required this.zoom, + this.onTap, + }) : super(key: key); + + @override + build(BuildContext context) { + if (GpsMapProvider.values[Pref().getGpsMapProviderOr(0)] == + GpsMapProvider.osm) { + return _OsmGpsMap( + center: center, + zoom: zoom, + onTap: onTap, + ); + } else { + return _GoogleGpsMap( + center: center, + zoom: zoom, + onTap: onTap, + ); + } + } + + /// A pair of latitude and longitude coordinates, stored as degrees + final Tuple2 center; + final double zoom; + final void Function()? onTap; +} + +typedef _GoogleGpsMap = platform.GoogleGpsMap; + +class _OsmGpsMap extends StatelessWidget { + const _OsmGpsMap({ + Key? key, + required this.center, + required this.zoom, + this.onTap, + }) : super(key: key); + + @override + build(BuildContext context) { + const double pinSize = 48; + final centerLl = LatLng(center.item1, center.item2); + return GestureDetector( + onTap: () { + launch( + "https://www.openstreetmap.org/?mlat=${center.item1}&mlon=${center.item2}#map=${zoom.toInt()}/${center.item1}/${center.item2}"); + }, + behavior: HitTestBehavior.opaque, + // IgnorePointer is needed to prevent FlutterMap absorbing all pointer + // events + child: IgnorePointer( + child: FlutterMap( + options: MapOptions( + center: centerLl, + zoom: zoom, + allowPanning: false, + enableScrollWheel: false, + interactiveFlags: InteractiveFlag.none, + ), + layers: [ + TileLayerOptions( + urlTemplate: "https://tile.openstreetmap.org/{z}/{x}/{y}.png", + attributionBuilder: (_) { + return const Text( + "© OpenStreetMap contributors", + style: TextStyle(color: Colors.black), + ); + }, + ), + MarkerLayerOptions( + markers: [ + Marker( + width: pinSize, + height: pinSize, + point: centerLl, + anchorPos: AnchorPos.align(AnchorAlign.top), + builder: (context) => const Image( + image: AssetImage("gps_map_pin.png"), + ), + ), + ], + ), + ], + ), + ), + ); + } + + final Tuple2 center; + final double zoom; + final void Function()? onTap; +} diff --git a/lib/widget/settings.dart b/lib/widget/settings.dart index 46dd5b69..3bdc01f7 100644 --- a/lib/widget/settings.dart +++ b/lib/widget/settings.dart @@ -17,6 +17,7 @@ import 'package:nc_photos/pref.dart'; import 'package:nc_photos/snack_bar_manager.dart'; import 'package:nc_photos/theme.dart'; import 'package:nc_photos/widget/fancy_option_picker.dart'; +import 'package:nc_photos/widget/gps_map.dart'; import 'package:nc_photos/widget/home.dart'; import 'package:nc_photos/widget/root_picker.dart'; import 'package:nc_photos/widget/share_folder_picker.dart'; @@ -105,17 +106,16 @@ class _SettingsState extends State { label: L10n.global().settingsAccountTitle, builder: () => AccountSettingsWidget(account: widget.account), ), - if (platform_k.isMobile) - _buildSubSettings( - context, - leading: Icon( - Icons.view_carousel_outlined, - color: AppTheme.getUnfocusedIconColor(context), - ), - label: L10n.global().settingsViewerTitle, - description: L10n.global().settingsViewerDescription, - builder: () => _ViewerSettings(), + _buildSubSettings( + context, + leading: Icon( + Icons.view_carousel_outlined, + color: AppTheme.getUnfocusedIconColor(context), ), + label: L10n.global().settingsViewerTitle, + description: L10n.global().settingsViewerDescription, + builder: () => _ViewerSettings(), + ), _buildSubSettings( context, leading: Icon( @@ -715,6 +715,7 @@ class _ViewerSettingsState extends State<_ViewerSettings> { super.initState(); _screenBrightness = Pref().getViewerScreenBrightnessOr(-1); _isForceRotation = Pref().isViewerForceRotationOr(false); + _gpsMapProvider = GpsMapProvider.values[Pref().getGpsMapProviderOr(0)]; } @override @@ -738,19 +739,27 @@ class _ViewerSettingsState extends State<_ViewerSettings> { SliverList( delegate: SliverChildListDelegate( [ - SwitchListTile( - title: Text(L10n.global().settingsScreenBrightnessTitle), - subtitle: - Text(L10n.global().settingsScreenBrightnessDescription), - value: _screenBrightness >= 0, - onChanged: (value) => - _onScreenBrightnessChanged(context, value), - ), - SwitchListTile( - title: Text(L10n.global().settingsForceRotationTitle), - subtitle: Text(L10n.global().settingsForceRotationDescription), - value: _isForceRotation, - onChanged: (value) => _onForceRotationChanged(value), + if (platform_k.isMobile) + SwitchListTile( + title: Text(L10n.global().settingsScreenBrightnessTitle), + subtitle: + Text(L10n.global().settingsScreenBrightnessDescription), + value: _screenBrightness >= 0, + onChanged: (value) => + _onScreenBrightnessChanged(context, value), + ), + if (platform_k.isMobile) + SwitchListTile( + title: Text(L10n.global().settingsForceRotationTitle), + subtitle: + Text(L10n.global().settingsForceRotationDescription), + value: _isForceRotation, + onChanged: (value) => _onForceRotationChanged(value), + ), + ListTile( + title: Text(L10n.global().settingsMapProviderTitle), + subtitle: Text(_gpsMapProvider.toUserString()), + onTap: () => _onMapProviderTap(context), ), ], ), @@ -789,7 +798,8 @@ class _ViewerSettingsState extends State<_ViewerSettings> { onChangeEnd: (value) async { brightness = value; try { - await ScreenBrightness().setScreenBrightness(value); + await ScreenBrightness() + .setScreenBrightness(value); } catch (e, stackTrace) { _log.severe("Failed while setScreenBrightness", e, stackTrace); @@ -830,6 +840,44 @@ class _ViewerSettingsState extends State<_ViewerSettings> { void _onForceRotationChanged(bool value) => _setForceRotation(value); + Future _onMapProviderTap(BuildContext context) async { + final oldValue = _gpsMapProvider; + final newValue = await showDialog( + context: context, + builder: (context) => FancyOptionPicker( + items: GpsMapProvider.values + .map((provider) => FancyOptionPickerItem( + label: provider.toUserString(), + isSelected: provider == oldValue, + onSelect: () { + _log.info( + "[_onMapProviderTap] Set map provider: ${provider.toUserString()}"); + Navigator.of(context).pop(provider); + }, + )) + .toList(), + ), + ); + if (newValue == null || newValue == oldValue) { + return; + } + setState(() { + _gpsMapProvider = newValue; + }); + try { + await Pref().setGpsMapProvider(newValue.index); + } catch (e, stackTrace) { + _log.severe("[_onMapProviderTap] Failed writing pref", e, stackTrace); + SnackBarManager().showSnackBar(SnackBar( + content: Text(L10n.global().writePreferenceFailureNotification), + duration: k.snackBarDurationNormal, + )); + setState(() { + _gpsMapProvider = oldValue; + }); + } + } + Future _setScreenBrightness(int value) async { final oldValue = _screenBrightness; setState(() { @@ -866,6 +914,7 @@ class _ViewerSettingsState extends State<_ViewerSettings> { late int _screenBrightness; late bool _isForceRotation; + late GpsMapProvider _gpsMapProvider; static final _log = Logger("widget.settings._ViewerSettingsState"); } diff --git a/lib/widget/viewer_detail_pane.dart b/lib/widget/viewer_detail_pane.dart index 1b2681cf..2c71d859 100644 --- a/lib/widget/viewer_detail_pane.dart +++ b/lib/widget/viewer_detail_pane.dart @@ -23,8 +23,6 @@ import 'package:nc_photos/entity/share/data_source.dart'; import 'package:nc_photos/exception_util.dart' as exception_util; import 'package:nc_photos/iterable_extension.dart'; import 'package:nc_photos/k.dart' as k; -import 'package:nc_photos/mobile/platform.dart' - if (dart.library.html) 'package:nc_photos/web/platform.dart' as platform; import 'package:nc_photos/notified_action.dart'; import 'package:nc_photos/platform/features.dart' as features; import 'package:nc_photos/platform/k.dart' as platform_k; @@ -36,6 +34,7 @@ import 'package:nc_photos/use_case/remove_from_album.dart'; import 'package:nc_photos/use_case/update_album.dart'; import 'package:nc_photos/use_case/update_property.dart'; import 'package:nc_photos/widget/album_picker_dialog.dart'; +import 'package:nc_photos/widget/gps_map.dart'; import 'package:nc_photos/widget/photo_date_time_edit_dialog.dart'; import 'package:path/path.dart'; import 'package:tuple/tuple.dart'; @@ -248,7 +247,7 @@ class _ViewerDetailPaneState extends State { if (features.isSupportMapView && _gps != null) SizedBox( height: 256, - child: platform.GpsMap( + child: GpsMap( center: _gps!, zoom: 16, onTap: _onMapTap, diff --git a/pubspec.lock b/pubspec.lock index ff36f0e5..b219cd09 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -309,6 +309,13 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_map: + dependency: "direct main" + description: + name: flutter_map + url: "https://pub.dartlang.org" + source: hosted + version: "0.14.0" flutter_plugin_android_lifecycle: dependency: transitive description: @@ -449,6 +456,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.1" + latlong2: + dependency: transitive + description: + name: latlong2 + url: "https://pub.dartlang.org" + source: hosted + version: "0.8.1" lints: dependency: transitive description: @@ -456,6 +470,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.1" + lists: + dependency: transitive + description: + name: lists + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.1" logging: dependency: "direct main" description: @@ -477,6 +498,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.7.0" + mgrs_dart: + dependency: transitive + description: + name: mgrs_dart + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" mime: dependency: transitive description: @@ -617,6 +645,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.5.0" + positioned_tap_detector_2: + dependency: transitive + description: + name: positioned_tap_detector_2 + url: "https://pub.dartlang.org" + source: hosted + version: "1.0.4" process: dependency: transitive description: @@ -624,6 +659,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.2.4" + proj4dart: + dependency: transitive + description: + name: proj4dart + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" provider: dependency: transitive description: @@ -846,6 +888,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.0" + transparent_image: + dependency: transitive + description: + name: transparent_image + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" tuple: dependency: "direct main" description: @@ -860,6 +909,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + unicode: + dependency: transitive + description: + name: unicode + url: "https://pub.dartlang.org" + source: hosted + version: "0.3.1" url_launcher: dependency: "direct main" description: @@ -1013,6 +1069,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.2.10" + wkt_parser: + dependency: transitive + description: + name: wkt_parser + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" woozy_search: dependency: "direct main" description: diff --git a/pubspec.yaml b/pubspec.yaml index 148e1ec3..1c3cb1f7 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -43,6 +43,7 @@ dependencies: url: https://gitlab.com/nkming2/exifdart.git ref: 1.1.0 flutter_bloc: ^7.0.0 + flutter_map: ^0.14.0 flutter_staggered_grid_view: git: url: https://gitlab.com/nkming2/flutter_staggered_grid_view