1
0
mirror of https://github.com/immich-app/immich.git synced 2024-12-24 10:37:28 +02:00
immich/mobile/lib/shared/ui/location_picker.dart
shenlong e6c0f0e3aa
refactor(mobile): maplibre (#6087)
* chore: maplibre gl pubspec

* refactor(wip): maplibre for maps

* refactor(wip): dual pane + location button

* chore: remove flutter_map and deps

* refactor(wip): map zoom to location

* refactor: location picker

* open gallery_viewer on marker tap

* remove detectScaleGesture param

* test: debounce and throttle

* chore: rename get location method

* feat(mobile): Adds gps locator to map prompt for easy geolocation (#6282)

* Refactored get gps coords

* Use var for linter's sake, should handle errors better

* Cleanup

* Fix linter issues

* chore(dep): update maplibre to official lib

---------

Co-authored-by: shenlong-tanwen <139912620+shalong-tanwen@users.noreply.github.com>
Co-authored-by: Joshua Herrera <joshua.herrera227@gmail.com>
2024-01-15 09:26:13 -06:00

269 lines
7.8 KiB
Dart

import 'package:auto_route/auto_route.dart';
import 'package:easy_localization/easy_localization.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:immich_mobile/extensions/build_context_extensions.dart';
import 'package:immich_mobile/extensions/string_extensions.dart';
import 'package:immich_mobile/modules/map/widgets/map_thumbnail.dart';
import 'package:immich_mobile/routing/router.dart';
import 'package:maplibre_gl/maplibre_gl.dart';
Future<LatLng?> showLocationPicker({
required BuildContext context,
LatLng? initialLatLng,
}) {
return showDialog<LatLng?>(
context: context,
useRootNavigator: false,
builder: (ctx) => _LocationPicker(
initialLatLng: initialLatLng,
),
);
}
enum _LocationPickerMode { map, manual }
class _LocationPicker extends HookWidget {
final LatLng? initialLatLng;
const _LocationPicker({
this.initialLatLng,
});
@override
Widget build(BuildContext context) {
final latitude = useState(initialLatLng?.latitude ?? 0.0);
final longitude = useState(initialLatLng?.longitude ?? 0.0);
final latlng = LatLng(latitude.value, longitude.value);
final pickerMode = useState(_LocationPickerMode.map);
Future<void> onMapTap() async {
final newLatLng = await context.pushRoute<LatLng?>(
MapLocationPickerRoute(initialLatLng: latlng),
);
if (newLatLng != null) {
latitude.value = newLatLng.latitude;
longitude.value = newLatLng.longitude;
}
}
return AlertDialog(
contentPadding: const EdgeInsets.all(30),
alignment: Alignment.center,
content: SingleChildScrollView(
child: pickerMode.value == _LocationPickerMode.map
? _MapPicker(
key: ValueKey(latlng),
latlng: latlng,
onModeSwitch: () =>
pickerMode.value = _LocationPickerMode.manual,
onMapTap: onMapTap,
)
: _ManualPicker(
latlng: latlng,
onModeSwitch: () => pickerMode.value = _LocationPickerMode.map,
onLatUpdated: (value) => latitude.value = value,
onLonUpdated: (value) => longitude.value = value,
),
),
actions: [
TextButton(
onPressed: () => context.pop(),
child: Text(
"action_common_cancel",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.colorScheme.error,
),
).tr(),
),
TextButton(
onPressed: () => context.popRoute(latlng),
child: Text(
"action_common_update",
style: context.textTheme.bodyMedium?.copyWith(
fontWeight: FontWeight.w600,
color: context.primaryColor,
),
).tr(),
),
],
);
}
}
class _ManualPickerInput extends HookWidget {
final String initialValue;
final String decorationText;
final String hintText;
final String errorText;
final FocusNode focusNode;
final bool Function(String value) validator;
final Function(double value) onUpdated;
const _ManualPickerInput({
required this.initialValue,
required this.decorationText,
required this.hintText,
required this.errorText,
required this.focusNode,
required this.validator,
required this.onUpdated,
});
@override
Widget build(BuildContext context) {
final isValid = useState(true);
final controller = useTextEditingController(text: initialValue);
void onEditingComplete() {
isValid.value = validator(controller.text);
if (isValid.value) {
onUpdated(controller.text.toDouble());
}
}
return TextField(
controller: controller,
focusNode: focusNode,
textInputAction: TextInputAction.done,
autofocus: false,
decoration: InputDecoration(
labelText: decorationText.tr(),
labelStyle: TextStyle(
fontWeight: FontWeight.bold,
color: context.primaryColor,
),
floatingLabelBehavior: FloatingLabelBehavior.auto,
border: const OutlineInputBorder(),
hintText: hintText.tr(),
hintStyle: const TextStyle(fontWeight: FontWeight.normal, fontSize: 14),
errorText: isValid.value ? null : errorText.tr(),
),
onEditingComplete: onEditingComplete,
keyboardType: const TextInputType.numberWithOptions(decimal: true),
inputFormatters: [LengthLimitingTextInputFormatter(8)],
onTapOutside: (_) => focusNode.unfocus(),
);
}
}
class _ManualPicker extends HookWidget {
final LatLng latlng;
final Function() onModeSwitch;
final Function(double) onLatUpdated;
final Function(double) onLonUpdated;
const _ManualPicker({
required this.latlng,
required this.onModeSwitch,
required this.onLatUpdated,
required this.onLonUpdated,
});
bool _validateLat(String value) {
final l = double.tryParse(value);
return l != null && l > -90 && l < 90;
}
bool _validateLong(String value) {
final l = double.tryParse(value);
return l != null && l > -180 && l < 180;
}
@override
Widget build(BuildContext context) {
final latitiudeFocusNode = useFocusNode();
final longitudeFocusNode = useFocusNode();
void onLatitudeUpdated(double value) {
onLatUpdated(value);
longitudeFocusNode.requestFocus();
}
void onLongitudeEditingCompleted(double value) {
onLonUpdated(value);
longitudeFocusNode.unfocus();
}
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"edit_location_dialog_title",
textAlign: TextAlign.center,
).tr(),
const SizedBox(height: 12),
TextButton.icon(
icon: const Text("location_picker_choose_on_map").tr(),
label: const Icon(Icons.map_outlined, size: 16),
onPressed: onModeSwitch,
),
const SizedBox(height: 12),
_ManualPickerInput(
initialValue: latlng.latitude.toStringAsFixed(4),
decorationText: "location_picker_latitude",
hintText: "location_picker_latitude_hint",
errorText: "location_picker_latitude_error",
focusNode: latitiudeFocusNode,
validator: _validateLat,
onUpdated: onLatitudeUpdated,
),
const SizedBox(height: 24),
_ManualPickerInput(
initialValue: latlng.longitude.toStringAsFixed(4),
decorationText: "location_picker_longitude",
hintText: "location_picker_longitude_hint",
errorText: "location_picker_longitude_error",
focusNode: latitiudeFocusNode,
validator: _validateLong,
onUpdated: onLongitudeEditingCompleted,
),
],
);
}
}
class _MapPicker extends StatelessWidget {
final LatLng latlng;
final Function() onModeSwitch;
final Function() onMapTap;
const _MapPicker({
required this.latlng,
required this.onModeSwitch,
required this.onMapTap,
super.key,
});
@override
Widget build(BuildContext context) {
return Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
"edit_location_dialog_title",
textAlign: TextAlign.center,
).tr(),
const SizedBox(height: 12),
TextButton.icon(
icon: Text(
"${latlng.latitude.toStringAsFixed(4)}, ${latlng.longitude.toStringAsFixed(4)}",
),
label: const Icon(Icons.edit_outlined, size: 16),
onPressed: onModeSwitch,
),
const SizedBox(height: 12),
MapThumbnail(
centre: latlng,
height: 200,
width: 200,
zoom: 8,
showMarkerPin: true,
onTap: (_, __) => onMapTap(),
),
],
);
}
}