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:flutter_map/plugin_api.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/string_extensions.dart'; import 'package:immich_mobile/modules/map/ui/map_thumbnail.dart'; import 'package:immich_mobile/routing/router.dart'; import 'package:latlong2/latlong.dart'; Future showLocationPicker({ required BuildContext context, LatLng? initialLatLng, }) { return showDialog( context: context, useRootNavigator: false, builder: (ctx) => _LocationPicker( initialLatLng: initialLatLng, ), ); } enum _LocationPickerMode { map, manual } 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; } 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); final latitudeController = useTextEditingController(); final isValidLatitude = useState(true); final latitiudeFocusNode = useFocusNode(); final longitudeController = useTextEditingController(); final longitudeFocusNode = useFocusNode(); final isValidLongitude = useState(true); void validateInputs() { isValidLatitude.value = _validateLat(latitudeController.text); if (isValidLatitude.value) { latitude.value = latitudeController.text.toDouble(); } isValidLongitude.value = _validateLong(longitudeController.text); if (isValidLongitude.value) { longitude.value = longitudeController.text.toDouble(); } } void validateAndPop() { if (pickerMode.value == _LocationPickerMode.manual) { validateInputs(); } if (isValidLatitude.value && isValidLongitude.value) { return context.pop(latlng); } } List buildMapPickerMode() { return [ TextButton.icon( icon: Text( "${latitude.value.toStringAsFixed(4)}, ${longitude.value.toStringAsFixed(4)}", ), label: const Icon(Icons.edit_outlined, size: 16), onPressed: () { latitudeController.text = latitude.value.toStringAsFixed(4); longitudeController.text = longitude.value.toStringAsFixed(4); pickerMode.value = _LocationPickerMode.manual; }, ), const SizedBox( height: 12, ), MapThumbnail( coords: latlng, height: 200, width: 200, zoom: 6, showAttribution: false, onTap: (p0, p1) async { final newLatLng = await context.pushRoute( MapLocationPickerRoute(initialLatLng: latlng), ); if (newLatLng != null) { latitude.value = newLatLng.latitude; longitude.value = newLatLng.longitude; } }, markers: [ Marker( anchorPos: AnchorPos.align(AnchorAlign.top), point: LatLng( latitude.value, longitude.value, ), builder: (ctx) => const Image( image: AssetImage('assets/location-pin.png'), ), ), ], ), ]; } List buildManualPickerMode() { return [ TextButton.icon( icon: const Text("location_picker_choose_on_map").tr(), label: const Icon(Icons.map_outlined, size: 16), onPressed: () { validateInputs(); if (isValidLatitude.value && isValidLongitude.value) { pickerMode.value = _LocationPickerMode.map; } }, ), const SizedBox( height: 12, ), TextField( controller: latitudeController, focusNode: latitiudeFocusNode, textInputAction: TextInputAction.done, autofocus: false, decoration: InputDecoration( labelText: 'location_picker_latitude'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, color: context.primaryColor, ), floatingLabelBehavior: FloatingLabelBehavior.auto, border: const OutlineInputBorder(), hintText: 'location_picker_latitude_hint'.tr(), hintStyle: const TextStyle( fontWeight: FontWeight.normal, fontSize: 14, ), errorText: isValidLatitude.value ? null : "location_picker_latitude_error".tr(), ), onEditingComplete: () { isValidLatitude.value = _validateLat(latitudeController.text); if (isValidLatitude.value) { latitude.value = latitudeController.text.toDouble(); longitudeFocusNode.requestFocus(); } }, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [LengthLimitingTextInputFormatter(8)], onTapOutside: (_) => latitiudeFocusNode.unfocus(), ), const SizedBox( height: 24, ), TextField( controller: longitudeController, focusNode: longitudeFocusNode, textInputAction: TextInputAction.done, autofocus: false, decoration: InputDecoration( labelText: 'location_picker_longitude'.tr(), labelStyle: TextStyle( fontWeight: FontWeight.bold, color: context.primaryColor, ), floatingLabelBehavior: FloatingLabelBehavior.auto, border: const OutlineInputBorder(), hintText: 'location_picker_longitude_hint'.tr(), hintStyle: const TextStyle( fontWeight: FontWeight.normal, fontSize: 14, ), errorText: isValidLongitude.value ? null : "location_picker_longitude_error".tr(), ), onEditingComplete: () { isValidLongitude.value = _validateLong(longitudeController.text); if (isValidLongitude.value) { longitude.value = longitudeController.text.toDouble(); longitudeFocusNode.unfocus(); } }, keyboardType: const TextInputType.numberWithOptions(decimal: true), inputFormatters: [LengthLimitingTextInputFormatter(8)], onTapOutside: (_) => longitudeFocusNode.unfocus(), ), ]; } return AlertDialog( contentPadding: const EdgeInsets.all(30), alignment: Alignment.center, content: SingleChildScrollView( child: Column( mainAxisSize: MainAxisSize.min, children: [ const Text( "edit_location_dialog_title", textAlign: TextAlign.center, ).tr(), const SizedBox( height: 12, ), if (pickerMode.value == _LocationPickerMode.manual) ...buildManualPickerMode(), if (pickerMode.value == _LocationPickerMode.map) ...buildMapPickerMode(), ], ), ), 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: validateAndPop, child: Text( "action_common_update", style: context.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, color: context.primaryColor, ), ).tr(), ), ], ); } }