From 92286372cbdafe8d134d41e0a730a4cea4bab451 Mon Sep 17 00:00:00 2001 From: Ming Ming Date: Wed, 30 Oct 2024 22:14:08 +0800 Subject: [PATCH] Add a place picker --- app/lib/l10n/app_en.arb | 1 + app/lib/l10n/untranslated-messages.txt | 41 +++++--- app/lib/widget/my_app.dart | 2 + app/lib/widget/place_picker/bloc.dart | 21 +++++ app/lib/widget/place_picker/place_picker.dart | 93 +++++++++++++++++++ .../widget/place_picker/place_picker.g.dart | 73 +++++++++++++++ app/lib/widget/place_picker/state_event.dart | 28 ++++++ np_gps_map/lib/np_gps_map.dart | 2 + np_gps_map/lib/src/interactive_map.dart | 5 + .../lib/src/interactive_map/google.dart | 16 +++- np_gps_map/lib/src/interactive_map/osm.dart | 8 ++ np_gps_map/lib/src/place_picker.dart | 46 +++++++++ np_gps_map/lib/src/type.dart | 45 +++++++++ np_gps_map/pubspec.yaml | 1 + 14 files changed, 368 insertions(+), 14 deletions(-) create mode 100644 app/lib/widget/place_picker/bloc.dart create mode 100644 app/lib/widget/place_picker/place_picker.dart create mode 100644 app/lib/widget/place_picker/place_picker.g.dart create mode 100644 app/lib/widget/place_picker/state_event.dart create mode 100644 np_gps_map/lib/src/place_picker.dart create mode 100644 np_gps_map/lib/src/type.dart diff --git a/app/lib/l10n/app_en.arb b/app/lib/l10n/app_en.arb index bfd3e57d..35bbf9de 100644 --- a/app/lib/l10n/app_en.arb +++ b/app/lib/l10n/app_en.arb @@ -1521,6 +1521,7 @@ "@customizeButtonsUnsupportedWarning": { "description": "Some button can't be removed. This message will be shown instead when user try to do so" }, + "placePickerTitle": "Pick a place", "errorUnauthenticated": "Unauthenticated access. Please sign-in again if the problem continues", "@errorUnauthenticated": { diff --git a/app/lib/l10n/untranslated-messages.txt b/app/lib/l10n/untranslated-messages.txt index 04bc7157..72dd89df 100644 --- a/app/lib/l10n/untranslated-messages.txt +++ b/app/lib/l10n/untranslated-messages.txt @@ -268,6 +268,7 @@ "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", + "placePickerTitle", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -286,7 +287,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "de": [ @@ -297,7 +299,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "el": [ @@ -453,7 +456,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "es": [ @@ -464,7 +468,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "fi": [ @@ -511,7 +516,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "fr": [ @@ -558,7 +564,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "it": [ @@ -610,7 +617,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "nl": [ @@ -999,6 +1007,7 @@ "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", "customizeButtonsUnsupportedWarning", + "placePickerTitle", "errorUnauthenticated", "errorDisconnected", "errorLocked", @@ -1057,7 +1066,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "pt": [ @@ -1124,7 +1134,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "ru": [ @@ -1171,7 +1182,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "tr": [ @@ -1182,7 +1194,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "zh": [ @@ -1260,7 +1273,8 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ], "zh_Hant": [ @@ -1432,6 +1446,7 @@ "livePhotoTooltip", "dragAndDropRearrangeButtons", "customizeCollectionsNavBarDescription", - "customizeButtonsUnsupportedWarning" + "customizeButtonsUnsupportedWarning", + "placePickerTitle" ] } diff --git a/app/lib/widget/my_app.dart b/app/lib/widget/my_app.dart index 76b21bd2..fb7a920f 100644 --- a/app/lib/widget/my_app.dart +++ b/app/lib/widget/my_app.dart @@ -38,6 +38,7 @@ import 'package:nc_photos/widget/image_enhancer.dart'; import 'package:nc_photos/widget/local_file_viewer.dart'; import 'package:nc_photos/widget/map_browser.dart'; import 'package:nc_photos/widget/people_browser.dart'; +import 'package:nc_photos/widget/place_picker/place_picker.dart'; import 'package:nc_photos/widget/places_browser.dart'; import 'package:nc_photos/widget/result_viewer.dart'; import 'package:nc_photos/widget/root_picker.dart'; @@ -213,6 +214,7 @@ class _WrappedAppState extends State<_WrappedApp> ArchiveBrowser.routeName: ArchiveBrowser.buildRoute, TrustedCertManager.routeName: TrustedCertManager.buildRoute, MapBrowser.routeName: MapBrowser.buildRoute, + PlacePicker.routeName: PlacePicker.buildRoute, }; Route? _onGenerateRoute(RouteSettings settings) { diff --git a/app/lib/widget/place_picker/bloc.dart b/app/lib/widget/place_picker/bloc.dart new file mode 100644 index 00000000..74ae7c87 --- /dev/null +++ b/app/lib/widget/place_picker/bloc.dart @@ -0,0 +1,21 @@ +part of 'place_picker.dart'; + +@npLog +class _Bloc extends Bloc<_Event, _State> with BlocLogger { + _Bloc() : super(_State.init()) { + on<_SetPosition>(_onSetPosition); + } + + @override + String get tag => _log.fullName; + + @override + bool Function(dynamic, dynamic)? get shouldLog => (currentState, nextState) { + return currentState.position == nextState.position; + }; + + void _onSetPosition(_SetPosition ev, _Emitter emit) { + // _log.info(ev); + emit(state.copyWith(position: ev.value)); + } +} diff --git a/app/lib/widget/place_picker/place_picker.dart b/app/lib/widget/place_picker/place_picker.dart new file mode 100644 index 00000000..d950c2b6 --- /dev/null +++ b/app/lib/widget/place_picker/place_picker.dart @@ -0,0 +1,93 @@ +import 'package:copy_with/copy_with.dart'; +import 'package:flutter/material.dart'; +import 'package:flutter_bloc/flutter_bloc.dart'; +import 'package:logging/logging.dart'; +import 'package:nc_photos/app_localizations.dart'; +import 'package:nc_photos/bloc_util.dart'; +import 'package:nc_photos/controller/pref_controller.dart'; +import 'package:nc_photos/stream_util.dart'; +import 'package:np_codegen/np_codegen.dart'; +import 'package:np_gps_map/np_gps_map.dart'; +import 'package:to_string/to_string.dart'; + +part 'bloc.dart'; +part 'place_picker.g.dart'; +part 'state_event.dart'; + +class PlacePicker extends StatelessWidget { + static const routeName = "/place-picker"; + + static Route buildRoute(RouteSettings settings) => + MaterialPageRoute( + builder: (_) => const PlacePicker(), + settings: settings, + ); + + const PlacePicker({super.key}); + + @override + Widget build(BuildContext context) { + return BlocProvider( + create: (context) => _Bloc(), + child: const _WrappedPlacePicker(), + ); + } +} + +@npLog +class _WrappedPlacePicker extends StatelessWidget { + const _WrappedPlacePicker(); + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(L10n.global().placePickerTitle), + leading: IconButton( + onPressed: () { + final position = context.state.position; + _log.info("[build] Position picked: $position"); + Navigator.of(context).pop(position); + }, + icon: const Icon(Icons.check_outlined), + ), + ), + body: const _BodyView(), + ); + } +} + +class _BodyView extends StatelessWidget { + const _BodyView(); + + @override + Widget build(BuildContext context) { + final prevPosition = + context.read().mapBrowserPrevPositionValue; + return ValueStreamBuilderEx( + stream: context.read().gpsMapProvider, + builder: StreamWidgetBuilder.value( + (context, gpsMapProvider) => PlacePickerView( + providerHint: gpsMapProvider, + initialPosition: prevPosition ?? const MapCoord(0, 0), + initialZoom: prevPosition == null ? 2.5 : 10, + onCameraMove: (position) { + context.addEvent(_SetPosition(position)); + }, + ), + ), + ); + } +} + +// typedef _BlocBuilder = BlocBuilder<_Bloc, _State>; +// typedef _BlocListener = BlocListener<_Bloc, _State>; +// typedef _BlocListenerT = BlocListenerT<_Bloc, _State, T>; +// typedef _BlocSelector = BlocSelector<_Bloc, _State, T>; +typedef _Emitter = Emitter<_State>; + +extension on BuildContext { + _Bloc get bloc => read<_Bloc>(); + _State get state => bloc.state; + void addEvent(_Event event) => bloc.add(event); +} diff --git a/app/lib/widget/place_picker/place_picker.g.dart b/app/lib/widget/place_picker/place_picker.g.dart new file mode 100644 index 00000000..f8d17d3d --- /dev/null +++ b/app/lib/widget/place_picker/place_picker.g.dart @@ -0,0 +1,73 @@ +// GENERATED CODE - DO NOT MODIFY BY HAND + +part of 'place_picker.dart'; + +// ************************************************************************** +// CopyWithLintRuleGenerator +// ************************************************************************** + +// ignore_for_file: library_private_types_in_public_api, duplicate_ignore + +// ************************************************************************** +// CopyWithGenerator +// ************************************************************************** + +abstract class $_StateCopyWithWorker { + _State call({CameraPosition? position}); +} + +class _$_StateCopyWithWorkerImpl implements $_StateCopyWithWorker { + _$_StateCopyWithWorkerImpl(this.that); + + @override + _State call({dynamic position = copyWithNull}) { + return _State( + position: position == copyWithNull + ? that.position + : position as CameraPosition?); + } + + final _State that; +} + +extension $_StateCopyWith on _State { + $_StateCopyWithWorker get copyWith => _$copyWith; + $_StateCopyWithWorker get _$copyWith => _$_StateCopyWithWorkerImpl(this); +} + +// ************************************************************************** +// NpLogGenerator +// ************************************************************************** + +extension _$_WrappedPlacePickerNpLog on _WrappedPlacePicker { + // ignore: unused_element + Logger get _log => log; + + static final log = + Logger("widget.place_picker.place_picker._WrappedPlacePicker"); +} + +extension _$_BlocNpLog on _Bloc { + // ignore: unused_element + Logger get _log => log; + + static final log = Logger("widget.place_picker.place_picker._Bloc"); +} + +// ************************************************************************** +// ToStringGenerator +// ************************************************************************** + +extension _$_StateToString on _State { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_State {position: $position}"; + } +} + +extension _$_SetPositionToString on _SetPosition { + String _$toString() { + // ignore: unnecessary_string_interpolations + return "_SetPosition {value: $value}"; + } +} diff --git a/app/lib/widget/place_picker/state_event.dart b/app/lib/widget/place_picker/state_event.dart new file mode 100644 index 00000000..ea405ac4 --- /dev/null +++ b/app/lib/widget/place_picker/state_event.dart @@ -0,0 +1,28 @@ +part of 'place_picker.dart'; + +@genCopyWith +@toString +class _State { + const _State({ + this.position, + }); + + factory _State.init() => const _State(); + + @override + String toString() => _$toString(); + + final CameraPosition? position; +} + +abstract class _Event {} + +@toString +class _SetPosition implements _Event { + const _SetPosition(this.value); + + @override + String toString() => _$toString(); + + final CameraPosition value; +} diff --git a/np_gps_map/lib/np_gps_map.dart b/np_gps_map/lib/np_gps_map.dart index 11e5edf1..24abb808 100644 --- a/np_gps_map/lib/np_gps_map.dart +++ b/np_gps_map/lib/np_gps_map.dart @@ -3,4 +3,6 @@ library np_gps_map; export 'src/gps_map.dart'; export 'src/interactive_map.dart'; export 'src/map_coord.dart'; +export 'src/place_picker.dart'; +export 'src/type.dart'; export 'src/util.dart' show initGpsMap; diff --git a/np_gps_map/lib/src/interactive_map.dart b/np_gps_map/lib/src/interactive_map.dart index d44ed04e..ef6777d9 100644 --- a/np_gps_map/lib/src/interactive_map.dart +++ b/np_gps_map/lib/src/interactive_map.dart @@ -3,6 +3,7 @@ import 'package:np_gps_map/src/gps_map.dart'; import 'package:np_gps_map/src/interactive_map/google.dart'; import 'package:np_gps_map/src/interactive_map/osm.dart'; import 'package:np_gps_map/src/map_coord.dart'; +import 'package:np_gps_map/src/type.dart'; import 'package:np_gps_map/src/util.dart'; import 'package:np_platform_util/np_platform_util.dart'; @@ -31,6 +32,7 @@ class InteractiveMap extends StatelessWidget { this.googleClusterBuilder, this.contentPadding, this.onMapCreated, + this.onCameraMove, }); @override @@ -45,6 +47,7 @@ class InteractiveMap extends StatelessWidget { clusterBuilder: osmClusterBuilder, contentPadding: contentPadding, onMapCreated: onMapCreated, + onCameraMove: onCameraMove, ); } else { return GoogleInteractiveMap( @@ -55,6 +58,7 @@ class InteractiveMap extends StatelessWidget { clusterBuilder: googleClusterBuilder, contentPadding: contentPadding, onMapCreated: onMapCreated, + onCameraMove: onCameraMove, ); } } @@ -70,4 +74,5 @@ class InteractiveMap extends StatelessWidget { final OsmClusterBuilder? osmClusterBuilder; final EdgeInsets? contentPadding; final void Function(InteractiveMapController controller)? onMapCreated; + final void Function(CameraPosition position)? onCameraMove; } diff --git a/np_gps_map/lib/src/interactive_map/google.dart b/np_gps_map/lib/src/interactive_map/google.dart index 9da9b182..5235cf87 100644 --- a/np_gps_map/lib/src/interactive_map/google.dart +++ b/np_gps_map/lib/src/interactive_map/google.dart @@ -3,9 +3,11 @@ import 'dart:async'; import 'package:flutter/material.dart'; import 'package:google_maps_cluster_manager/google_maps_cluster_manager.dart'; import 'package:google_maps_flutter/google_maps_flutter.dart'; +import 'package:latlong2/latlong.dart' as type; import 'package:np_common/object_util.dart'; import 'package:np_gps_map/src/interactive_map.dart'; import 'package:np_gps_map/src/map_coord.dart'; +import 'package:np_gps_map/src/type.dart' as type; typedef GoogleClusterBuilder = FutureOr Function( BuildContext context, List dataPoints); @@ -20,6 +22,7 @@ class GoogleInteractiveMap extends StatefulWidget { this.onClusterTap, this.contentPadding, this.onMapCreated, + this.onCameraMove, }); @override @@ -32,6 +35,7 @@ class GoogleInteractiveMap extends StatefulWidget { final void Function(List dataPoints)? onClusterTap; final EdgeInsets? contentPadding; final void Function(InteractiveMapController controller)? onMapCreated; + final void Function(type.CameraPosition position)? onCameraMove; } class _GoogleInteractiveMapState extends State { @@ -57,7 +61,17 @@ class _GoogleInteractiveMapState extends State { const CameraPosition(target: LatLng(0, 0)), markers: _markers, onMapCreated: _onMapCreated, - onCameraMove: _clusterManager.onCameraMove, + onCameraMove: (position) { + _clusterManager.onCameraMove(position); + widget.onCameraMove?.call(type.CameraPosition( + center: type.LatLng( + position.target.latitude, + position.target.longitude, + ), + zoom: position.zoom, + rotation: position.bearing, + )); + }, onCameraIdle: _clusterManager.updateMap, padding: widget.contentPadding ?? EdgeInsets.zero, ); diff --git a/np_gps_map/lib/src/interactive_map/osm.dart b/np_gps_map/lib/src/interactive_map/osm.dart index 8f5215b2..531d0297 100644 --- a/np_gps_map/lib/src/interactive_map/osm.dart +++ b/np_gps_map/lib/src/interactive_map/osm.dart @@ -8,6 +8,7 @@ import 'package:latlong2/latlong.dart'; import 'package:np_common/object_util.dart'; import 'package:np_gps_map/src/interactive_map.dart'; import 'package:np_gps_map/src/map_coord.dart'; +import 'package:np_gps_map/src/type.dart'; import 'package:rxdart/rxdart.dart'; typedef OsmClusterBuilder = Widget Function( @@ -23,6 +24,7 @@ class OsmInteractiveMap extends StatefulWidget { this.onClusterTap, this.contentPadding, this.onMapCreated, + this.onCameraMove, }); @override @@ -35,6 +37,7 @@ class OsmInteractiveMap extends StatefulWidget { final void Function(List dataPoints)? onClusterTap; final EdgeInsets? contentPadding; final void Function(InteractiveMapController controller)? onMapCreated; + final void Function(CameraPosition position)? onCameraMove; } class _OsmInteractiveMapState extends State { @@ -47,6 +50,11 @@ class _OsmInteractiveMapState extends State { widget.onMapCreated?.call(_parentController!); _subscriptions.add(_controller.mapEventStream.listen((ev) { _mapRotationRadSubject.add(ev.camera.rotationRad); + widget.onCameraMove?.call(CameraPosition( + center: ev.camera.center, + zoom: ev.camera.zoom, + rotation: (360 - ev.camera.rotation) % 360, + )); })); } }); diff --git a/np_gps_map/lib/src/place_picker.dart b/np_gps_map/lib/src/place_picker.dart new file mode 100644 index 00000000..c01f45c8 --- /dev/null +++ b/np_gps_map/lib/src/place_picker.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; +import 'package:np_gps_map/src/gps_map.dart'; +import 'package:np_gps_map/src/interactive_map.dart'; +import 'package:np_gps_map/src/map_coord.dart'; +import 'package:np_gps_map/src/type.dart'; + +class PlacePickerView extends StatelessWidget { + const PlacePickerView({ + super.key, + required this.providerHint, + this.initialPosition, + this.initialZoom, + this.contentPadding, + this.onCameraMove, + }); + + @override + Widget build(BuildContext context) { + return Stack( + children: [ + InteractiveMap( + providerHint: providerHint, + initialPosition: initialPosition, + initialZoom: initialZoom, + contentPadding: contentPadding, + onCameraMove: onCameraMove, + ), + Positioned.fill( + child: Transform.translate( + // 48(height) / 2 + offset: const Offset(0, -24), + child: Center( + child: Image.asset("packages/np_gps_map/assets/gps_map_pin.png"), + ), + ), + ), + ], + ); + } + + final GpsMapProvider providerHint; + final MapCoord? initialPosition; + final double? initialZoom; + final EdgeInsets? contentPadding; + final void Function(CameraPosition position)? onCameraMove; +} diff --git a/np_gps_map/lib/src/type.dart b/np_gps_map/lib/src/type.dart new file mode 100644 index 00000000..168a5c5c --- /dev/null +++ b/np_gps_map/lib/src/type.dart @@ -0,0 +1,45 @@ +import 'package:equatable/equatable.dart'; +import 'package:latlong2/latlong.dart'; +import 'package:np_common/type.dart'; + +class CameraPosition with EquatableMixin { + const CameraPosition({ + required this.center, + required this.zoom, + required this.rotation, + }); + + factory CameraPosition.fromJson(JsonObj json) { + return CameraPosition( + center: LatLng.fromJson(json["center"]), + zoom: json["zoom"], + rotation: json["rotation"], + ); + } + + @override + String toString() => "CameraPosition {" + "center: $center, " + "zoom: $zoom, " + "rotation: $rotation, " + "}"; + + JsonObj toJson() { + return { + "center": center.toJson(), + "zoom": zoom, + "rotation": rotation, + }; + } + + @override + List get props => [center, zoom, rotation]; + + final LatLng center; + final double zoom; + // The camera's bearing in degrees, measured clockwise from north. + // + // A bearing of 0.0, the default, means the camera points north. + // A bearing of 90.0 means the camera points east. + final double rotation; +} diff --git a/np_gps_map/pubspec.yaml b/np_gps_map/pubspec.yaml index 042798a4..8c724dee 100644 --- a/np_gps_map/pubspec.yaml +++ b/np_gps_map/pubspec.yaml @@ -9,6 +9,7 @@ environment: flutter: ">=3.19.0" dependencies: + equatable: ^2.0.5 flutter: sdk: flutter flutter_map: ^6.1.0