From 2a281e7906146070cd69db5f1a7c6a6dc4c35c9a Mon Sep 17 00:00:00 2001 From: Brandon Wees Date: Tue, 18 Nov 2025 21:06:51 -0600 Subject: [PATCH] feat(mobile): location edit from asset viewer (#23925) * chore: break sheet tile into own file * feat: set location from bottom sheet * refactor: location picker There was a lot of confusing controls here, simplified to 1 mode * fix: local asset check * chore: refactoring of location details widget * fix: update currentAssetExifProvider when changing location * chore: use SheetTile for location header * chore: remove coordinate change check * chore: remove comment --- i18n/en.json | 1 + .../asset_viewer/bottom_sheet.widget.dart | 88 +------- .../sheet_location_details.widget.dart | 90 ++++++--- .../asset_viewer/sheet_tile.widget.dart | 78 +++++++ .../infrastructure/action.provider.dart | 7 + .../lib/widgets/common/location_picker.dart | 191 ++++++------------ 6 files changed, 217 insertions(+), 238 deletions(-) create mode 100644 mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart diff --git a/i18n/en.json b/i18n/en.json index 8c4ee068fd..2b69858f9a 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1450,6 +1450,7 @@ "no_favorites_message": "Add favorites to quickly find your best pictures and videos", "no_libraries_message": "Create an external library to view your photos and videos", "no_local_assets_found": "No local assets found with this checksum", + "no_location_set": "No location set", "no_locked_photos_message": "Photos and videos in the locked folder are hidden and won't show up as you browse or search your library.", "no_name": "No Name", "no_notifications": "No notifications", diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart index c7c502194a..582a33136a 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet.widget.dart @@ -4,7 +4,6 @@ import 'package:auto_route/auto_route.dart'; import 'package:collection/collection.dart'; import 'package:easy_localization/easy_localization.dart'; import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; @@ -16,8 +15,8 @@ import 'package:immich_mobile/presentation/widgets/album/album_tile.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/asset_viewer.state.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart'; import 'package:immich_mobile/presentation/widgets/asset_viewer/bottom_sheet/sheet_people_details.widget.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; import 'package:immich_mobile/presentation/widgets/bottom_sheet/base_bottom_sheet.widget.dart'; -import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/album.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; @@ -181,7 +180,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { spacing: 12, children: [ if (albums.isNotEmpty) - _SheetTile( + SheetTile( title: 'appears_in'.t(context: context).toUpperCase(), titleStyle: context.textTheme.labelMedium?.copyWith( color: context.textTheme.labelMedium?.color?.withAlpha(200), @@ -233,7 +232,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { future: assetMediaRepository.getOriginalFilename(asset.id), builder: (context, snapshot) { final displayName = snapshot.data ?? asset.name; - return _SheetTile( + return SheetTile( title: displayName, titleStyle: context.textTheme.labelLarge, leading: Icon( @@ -250,7 +249,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { ); } else { // For remote assets, use the name directly - return _SheetTile( + return SheetTile( title: asset.name, titleStyle: context.textTheme.labelLarge, leading: Icon( @@ -269,7 +268,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { return SliverList.list( children: [ // Asset Date and Time - _SheetTile( + SheetTile( title: _getDateTime(context, asset), titleStyle: context.textTheme.bodyMedium?.copyWith(fontWeight: FontWeight.w600), trailing: asset.hasRemote && isOwner ? const Icon(Icons.edit, size: 18) : null, @@ -279,7 +278,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { const SheetPeopleDetails(), const SheetLocationDetails(), // Details header - _SheetTile( + SheetTile( title: 'exif_bottom_sheet_details'.t(context: context), titleStyle: context.textTheme.labelMedium?.copyWith( color: context.textTheme.labelMedium?.color?.withAlpha(200), @@ -290,7 +289,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { buildFileInfoTile(), // Camera info if (cameraTitle != null) - _SheetTile( + SheetTile( title: cameraTitle, titleStyle: context.textTheme.labelLarge, leading: Icon(Icons.camera_alt_outlined, size: 24, color: context.textTheme.labelLarge?.color), @@ -301,7 +300,7 @@ class _AssetDetailBottomSheet extends ConsumerWidget { ), // Lens info if (lensTitle != null) - _SheetTile( + SheetTile( title: lensTitle, titleStyle: context.textTheme.labelLarge, leading: Icon(Icons.camera_outlined, size: 24, color: context.textTheme.labelLarge?.color), @@ -319,77 +318,6 @@ class _AssetDetailBottomSheet extends ConsumerWidget { } } -class _SheetTile extends ConsumerWidget { - final String title; - final Widget? leading; - final Widget? trailing; - final String? subtitle; - final TextStyle? titleStyle; - final TextStyle? subtitleStyle; - final VoidCallback? onTap; - - const _SheetTile({ - required this.title, - this.titleStyle, - this.leading, - this.subtitle, - this.subtitleStyle, - this.trailing, - this.onTap, - }); - - void copyTitle(BuildContext context, WidgetRef ref) { - Clipboard.setData(ClipboardData(text: title)); - ImmichToast.show( - context: context, - msg: 'copied_to_clipboard'.t(context: context), - toastType: ToastType.info, - ); - ref.read(hapticFeedbackProvider.notifier).selectionClick(); - } - - @override - Widget build(BuildContext context, WidgetRef ref) { - final Widget titleWidget; - if (leading == null) { - titleWidget = LimitedBox( - maxWidth: double.infinity, - child: Text(title, style: titleStyle), - ); - } else { - titleWidget = Container( - width: double.infinity, - padding: const EdgeInsets.only(left: 15), - child: Text(title, style: titleStyle), - ); - } - - final Widget? subtitleWidget; - if (leading == null && subtitle != null) { - subtitleWidget = Text(subtitle!, style: subtitleStyle); - } else if (leading != null && subtitle != null) { - subtitleWidget = Padding( - padding: const EdgeInsets.only(left: 15), - child: Text(subtitle!, style: subtitleStyle), - ); - } else { - subtitleWidget = null; - } - - return ListTile( - dense: true, - visualDensity: VisualDensity.compact, - title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget), - titleAlignment: ListTileTitleAlignment.center, - leading: leading, - trailing: trailing, - contentPadding: leading == null ? null : const EdgeInsets.only(left: 25), - subtitle: subtitleWidget, - onTap: onTap, - ); - } -} - class _SheetAssetDescription extends ConsumerStatefulWidget { final ExifInfo exif; final bool isEditable; diff --git a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart index ab57ea4d8b..05d19476c6 100644 --- a/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart +++ b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart @@ -1,9 +1,12 @@ import 'package:flutter/material.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/constants/enums.dart'; import 'package:immich_mobile/domain/models/asset/base_asset.model.dart'; import 'package:immich_mobile/domain/models/exif.model.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/presentation/widgets/asset_viewer/sheet_tile.widget.dart'; +import 'package:immich_mobile/providers/infrastructure/action.provider.dart'; import 'package:immich_mobile/providers/infrastructure/asset_viewer/current_asset.provider.dart'; import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; import 'package:maplibre_gl/maplibre_gl.dart'; @@ -16,8 +19,6 @@ class SheetLocationDetails extends ConsumerStatefulWidget { } class _SheetLocationDetailsState extends ConsumerState { - BaseAsset? asset; - ExifInfo? exifInfo; MapLibreMapController? _mapController; String? _getLocationName(ExifInfo? exifInfo) { @@ -39,14 +40,11 @@ class _SheetLocationDetailsState extends ConsumerState { } void _onExifChanged(AsyncValue? previous, AsyncValue current) { - asset = ref.read(currentAssetNotifier); - setState(() { - exifInfo = current.valueOrNull; - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - if (exifInfo != null && hasCoordinates) { - _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(exifInfo!.latitude!, exifInfo!.longitude!))); - } - }); + final currentExif = current.valueOrNull; + + if (currentExif != null && currentExif.hasCoordinates) { + _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!))); + } } @override @@ -55,45 +53,71 @@ class _SheetLocationDetailsState extends ConsumerState { ref.listenManual(currentAssetExifProvider, _onExifChanged, fireImmediately: true); } + void editLocation() async { + await ref.read(actionProvider.notifier).editLocation(ActionSource.viewer, context); + } + @override Widget build(BuildContext context) { + final asset = ref.watch(currentAssetNotifier); + final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final hasCoordinates = exifInfo?.hasCoordinates ?? false; - // Guard no lat/lng - if (!hasCoordinates || (asset != null && asset is LocalAsset && asset!.hasRemote)) { + // Guard local assets + if (asset != null && asset is LocalAsset && asset.hasRemote) { return const SizedBox.shrink(); } - final remoteId = asset is LocalAsset ? (asset as LocalAsset).remoteId : (asset as RemoteAsset).id; + final remoteId = asset is LocalAsset ? asset.remoteId : (asset as RemoteAsset).id; final locationName = _getLocationName(exifInfo); - final coordinates = "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}"; + final coordinates = "${exifInfo?.latitude?.toStringAsFixed(4)}, ${exifInfo?.longitude?.toStringAsFixed(4)}"; return Padding( - padding: EdgeInsets.symmetric(vertical: 16.0, horizontal: context.isMobile ? 16.0 : 56.0), + padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: const EdgeInsets.only(bottom: 16), - child: Text( - "exif_bottom_sheet_location".t(context: context), - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, + SheetTile( + title: 'exif_bottom_sheet_location'.t(context: context), + titleStyle: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + trailing: hasCoordinates ? const Icon(Icons.edit_location_alt, size: 20) : null, + onTap: editLocation, + ), + if (hasCoordinates) + Padding( + padding: EdgeInsets.symmetric(horizontal: context.isMobile ? 16.0 : 56.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated), + const SizedBox(height: 16), + if (locationName != null) + Padding( + padding: const EdgeInsets.only(bottom: 4.0), + child: Text(locationName, style: context.textTheme.labelLarge), + ), + Text( + coordinates, + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(150), + ), + ), + ], ), ), - ), - ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated), - const SizedBox(height: 15), - if (locationName != null) - Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text(locationName, style: context.textTheme.labelLarge), + if (!hasCoordinates) + SheetTile( + title: "add_a_location".t(context: context), + titleStyle: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + leading: const Icon(Icons.location_off), + onTap: editLocation, ), - Text( - coordinates, - style: context.textTheme.labelMedium?.copyWith(color: context.textTheme.labelMedium?.color?.withAlpha(150)), - ), ], ), ); diff --git a/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart new file mode 100644 index 0000000000..e78aa926aa --- /dev/null +++ b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart @@ -0,0 +1,78 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/translate_extensions.dart'; +import 'package:immich_mobile/providers/haptic_feedback.provider.dart'; +import 'package:immich_mobile/widgets/common/immich_toast.dart'; + +class SheetTile extends ConsumerWidget { + final String title; + final Widget? leading; + final Widget? trailing; + final String? subtitle; + final TextStyle? titleStyle; + final TextStyle? subtitleStyle; + final VoidCallback? onTap; + + const SheetTile({ + super.key, + required this.title, + this.titleStyle, + this.leading, + this.subtitle, + this.subtitleStyle, + this.trailing, + this.onTap, + }); + + void copyTitle(BuildContext context, WidgetRef ref) { + Clipboard.setData(ClipboardData(text: title)); + ImmichToast.show( + context: context, + msg: 'copied_to_clipboard'.t(context: context), + toastType: ToastType.info, + ); + ref.read(hapticFeedbackProvider.notifier).selectionClick(); + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final Widget titleWidget; + if (leading == null) { + titleWidget = LimitedBox( + maxWidth: double.infinity, + child: Text(title, style: titleStyle), + ); + } else { + titleWidget = Container( + width: double.infinity, + padding: const EdgeInsets.only(left: 15), + child: Text(title, style: titleStyle), + ); + } + + final Widget? subtitleWidget; + if (leading == null && subtitle != null) { + subtitleWidget = Text(subtitle!, style: subtitleStyle); + } else if (leading != null && subtitle != null) { + subtitleWidget = Padding( + padding: const EdgeInsets.only(left: 15), + child: Text(subtitle!, style: subtitleStyle), + ); + } else { + subtitleWidget = null; + } + + return ListTile( + dense: true, + visualDensity: VisualDensity.compact, + title: GestureDetector(onLongPress: () => copyTitle(context, ref), child: titleWidget), + titleAlignment: ListTileTitleAlignment.center, + leading: leading, + trailing: trailing, + contentPadding: leading == null ? null : const EdgeInsets.only(left: 25), + subtitle: subtitleWidget, + onTap: onTap, + ); + } +} diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 659bd05db9..d4d850d8c1 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -301,6 +301,13 @@ class ActionNotifier extends Notifier { return null; } + // This must be called since editing location + // does not update the currentAsset which means + // the exif provider will not be refreshed automatically + if (source == ActionSource.viewer) { + ref.invalidate(currentAssetExifProvider); + } + return ActionResult(count: ids.length, success: true); } catch (error, stack) { _logger.severe('Failed to edit location for assets', error, stack); diff --git a/mobile/lib/widgets/common/location_picker.dart b/mobile/lib/widgets/common/location_picker.dart index 1f63299dd7..4736b182ed 100644 --- a/mobile/lib/widgets/common/location_picker.dart +++ b/mobile/lib/widgets/common/location_picker.dart @@ -5,7 +5,6 @@ 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'; @@ -17,19 +16,36 @@ Future showLocationPicker({required BuildContext context, LatLng? initi ); } -enum _LocationPickerMode { map, manual } - class _LocationPicker extends HookWidget { final LatLng? initialLatLng; const _LocationPicker({this.initialLatLng}); + 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 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 latitiudeFocusNode = useFocusNode(); + final longitudeFocusNode = useFocusNode(); + final latitudeController = useTextEditingController(text: latitude.value.toStringAsFixed(4)); + final longitudeController = useTextEditingController(text: longitude.value.toStringAsFixed(4)); + + useEffect(() { + latitudeController.text = latitude.value.toStringAsFixed(4); + longitudeController.text = longitude.value.toStringAsFixed(4); + return null; + }, [latitude.value, longitude.value]); Future onMapTap() async { final newLatLng = await context.pushRoute(MapLocationPickerRoute(initialLatLng: latlng)); @@ -39,23 +55,55 @@ class _LocationPicker extends HookWidget { } } + void onLatitudeUpdated(double value) { + latitude.value = value; + longitudeFocusNode.requestFocus(); + } + + void onLongitudeEditingCompleted(double value) { + longitude.value = value; + longitudeFocusNode.unfocus(); + } + 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, + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text("edit_location_dialog_title", style: context.textTheme.titleMedium).tr(), + Align( + alignment: Alignment.center, + child: TextButton.icon( + icon: const Text("location_picker_choose_on_map").tr(), + label: const Icon(Icons.map_outlined, size: 16), + onPressed: onMapTap, ), + ), + const SizedBox(height: 12), + _ManualPickerInput( + controller: latitudeController, + decorationText: "latitude", + hintText: "location_picker_latitude_hint", + errorText: "location_picker_latitude_error", + focusNode: latitiudeFocusNode, + validator: _validateLat, + onUpdated: onLatitudeUpdated, + ), + const SizedBox(height: 24), + _ManualPickerInput( + controller: longitudeController, + decorationText: "longitude", + hintText: "location_picker_longitude_hint", + errorText: "location_picker_longitude_error", + focusNode: longitudeFocusNode, + validator: _validateLong, + onUpdated: onLongitudeEditingCompleted, + ), + ], + ), ), actions: [ TextButton( @@ -81,7 +129,7 @@ class _LocationPicker extends HookWidget { } class _ManualPickerInput extends HookWidget { - final String initialValue; + final TextEditingController controller; final String decorationText; final String hintText; final String errorText; @@ -90,7 +138,7 @@ class _ManualPickerInput extends HookWidget { final Function(double value) onUpdated; const _ManualPickerInput({ - required this.initialValue, + required this.controller, required this.decorationText, required this.hintText, required this.errorText, @@ -101,7 +149,6 @@ class _ManualPickerInput extends HookWidget { @override Widget build(BuildContext context) { final isValid = useState(true); - final controller = useTextEditingController(text: initialValue); void onEditingComplete() { isValid.value = validator(controller.text); @@ -131,109 +178,3 @@ class _ManualPickerInput extends HookWidget { ); } } - -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(), - ), - ], - ); - } -}