mirror of
https://github.com/immich-app/immich.git
synced 2024-12-24 10:37:28 +02:00
e6c0f0e3aa
* 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>
269 lines
7.8 KiB
Dart
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(),
|
|
),
|
|
],
|
|
);
|
|
}
|
|
}
|