mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-30 18:22:37 -04:00 
			
		
		
		
	* feat(mobile): use Weblate for i18n (2) * remove old translation files * dedup keys * remove migration report * chore * remove localizely.yml
		
			
				
	
	
		
			270 lines
		
	
	
		
			7.8 KiB
		
	
	
	
		
			Dart
		
	
	
	
	
	
			
		
		
	
	
			270 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/widgets/map/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(
 | |
|             "cancel",
 | |
|             style: context.textTheme.bodyMedium?.copyWith(
 | |
|               fontWeight: FontWeight.w600,
 | |
|               color: context.colorScheme.error,
 | |
|             ),
 | |
|           ).tr(),
 | |
|         ),
 | |
|         TextButton(
 | |
|           onPressed: () => context.maybePop(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, signed: 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: "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: "longitude",
 | |
|           hintText: "location_picker_longitude_hint",
 | |
|           errorText: "location_picker_longitude_error",
 | |
|           focusNode: longitudeFocusNode,
 | |
|           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(),
 | |
|         ),
 | |
|       ],
 | |
|     );
 | |
|   }
 | |
| }
 |