Add a place picker

This commit is contained in:
Ming Ming 2024-10-30 22:14:08 +08:00
parent 0c78419c72
commit 92286372cb
14 changed files with 368 additions and 14 deletions

View file

@ -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": {

View file

@ -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"
]
}

View file

@ -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<dynamic>? _onGenerateRoute(RouteSettings settings) {

View file

@ -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));
}
}

View file

@ -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<CameraPosition>(
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<PrefController>().mapBrowserPrevPositionValue;
return ValueStreamBuilderEx<GpsMapProvider>(
stream: context.read<PrefController>().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<T> = BlocListenerT<_Bloc, _State, T>;
// typedef _BlocSelector<T> = 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);
}

View file

@ -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}";
}
}

View file

@ -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;
}

View file

@ -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;

View file

@ -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;
}

View file

@ -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<BitmapDescriptor> Function(
BuildContext context, List<DataPoint> 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<DataPoint> dataPoints)? onClusterTap;
final EdgeInsets? contentPadding;
final void Function(InteractiveMapController controller)? onMapCreated;
final void Function(type.CameraPosition position)? onCameraMove;
}
class _GoogleInteractiveMapState extends State<GoogleInteractiveMap> {
@ -57,7 +61,17 @@ class _GoogleInteractiveMapState extends State<GoogleInteractiveMap> {
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,
);

View file

@ -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<DataPoint> dataPoints)? onClusterTap;
final EdgeInsets? contentPadding;
final void Function(InteractiveMapController controller)? onMapCreated;
final void Function(CameraPosition position)? onCameraMove;
}
class _OsmInteractiveMapState extends State<OsmInteractiveMap> {
@ -47,6 +50,11 @@ class _OsmInteractiveMapState extends State<OsmInteractiveMap> {
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,
));
}));
}
});

View file

@ -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;
}

View file

@ -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<Object?> 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;
}

View file

@ -9,6 +9,7 @@ environment:
flutter: ">=3.19.0"
dependencies:
equatable: ^2.0.5
flutter:
sdk: flutter
flutter_map: ^6.1.0