From d5e35bc823a0e34dbf875c367a7fb9fcb505131a Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 15 Nov 2025 14:11:19 -0600 Subject: [PATCH 1/9] chore: break sheet tile into own file --- .../asset_viewer/bottom_sheet.widget.dart | 88 ++----------------- .../asset_viewer/sheet_tile.widget.dart | 78 ++++++++++++++++ 2 files changed, 86 insertions(+), 80 deletions(-) create mode 100644 mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart 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 d29e09a247a94..540810cb233f9 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/sheet_tile.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/sheet_tile.widget.dart new file mode 100644 index 0000000000000..e78aa926aa899 --- /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, + ); + } +} From 49e05e744f031c4377f32b61b48270128b6ecc00 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 15 Nov 2025 14:11:37 -0600 Subject: [PATCH 2/9] feat: set location from bottom sheet --- i18n/en.json | 1 + .../sheet_location_details.widget.dart | 78 ++++++++++++++----- 2 files changed, 59 insertions(+), 20 deletions(-) diff --git a/i18n/en.json b/i18n/en.json index 6da205d85aed3..9ae5ff0978068 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -1440,6 +1440,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/sheet_location_details.widget.dart b/mobile/lib/presentation/widgets/asset_viewer/bottom_sheet/sheet_location_details.widget.dart index ab57ea4d8be7d..4d53907838ec9 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'; @@ -59,41 +62,76 @@ class _SheetLocationDetailsState extends ConsumerState { Widget build(BuildContext context) { final hasCoordinates = exifInfo?.hasCoordinates ?? false; - // Guard no lat/lng - if (!hasCoordinates || (asset != null && asset is LocalAsset && asset!.hasRemote)) { + void editLocation() async { + await ref.read(actionProvider.notifier).editLocation(ActionSource.viewer, context); + } + + // Guard local assets + if (asset is! RemoteAsset) { return const SizedBox.shrink(); } final remoteId = asset is LocalAsset ? (asset as LocalAsset).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.symmetric(vertical: 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, - ), + padding: EdgeInsets.symmetric(horizontal: context.isMobile ? 16.0 : 56.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Text( + "exif_bottom_sheet_location".t(context: context), + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ), + if (hasCoordinates) IconButton(onPressed: editLocation, icon: const Icon(Icons.edit), iconSize: 20), + ], ), ), - ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated), - const SizedBox(height: 15), - if (locationName != null) + 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: 15), + 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), + ), + ), + ], + ), + ), + if (!hasCoordinates) Padding( - padding: const EdgeInsets.only(bottom: 4.0), - child: Text(locationName, style: context.textTheme.labelLarge), + padding: const EdgeInsets.only(top: 12), + child: 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)), - ), ], ), ); From 9eb951cccd9462737644f852325c8d1edec8ce20 Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 15 Nov 2025 14:12:02 -0600 Subject: [PATCH 3/9] refactor: location picker There was a lot of confusing controls here, simplified to 1 mode --- .../lib/widgets/common/location_picker.dart | 191 ++++++------------ 1 file changed, 66 insertions(+), 125 deletions(-) diff --git a/mobile/lib/widgets/common/location_picker.dart b/mobile/lib/widgets/common/location_picker.dart index 1f63299dd72e3..4736b182edc9f 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(), - ), - ], - ); - } -} From 0a2a986d7e1374d3ab9d5a4a03e4984106a32e5f Mon Sep 17 00:00:00 2001 From: bwees Date: Sat, 15 Nov 2025 14:23:27 -0600 Subject: [PATCH 4/9] fix: local asset check --- .../bottom_sheet/sheet_location_details.widget.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 4d53907838ec9..2641c4d028aaa 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 @@ -67,7 +67,7 @@ class _SheetLocationDetailsState extends ConsumerState { } // Guard local assets - if (asset is! RemoteAsset) { + if (asset != null && asset is LocalAsset && asset!.hasRemote) { return const SizedBox.shrink(); } From f6a5334c9225ee706277ba15dd5d085f418a6f54 Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 16 Nov 2025 12:25:45 -0600 Subject: [PATCH 5/9] chore: refactoring of location details widget --- .../sheet_location_details.widget.dart | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) 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 2641c4d028aaa..40a503c785ab9 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 @@ -19,8 +19,6 @@ class SheetLocationDetails extends ConsumerStatefulWidget { } class _SheetLocationDetailsState extends ConsumerState { - BaseAsset? asset; - ExifInfo? exifInfo; MapLibreMapController? _mapController; String? _getLocationName(ExifInfo? exifInfo) { @@ -42,14 +40,20 @@ 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 prevExif = previous?.valueOrNull; + final currentExif = current.valueOrNull; + + // Update map camera if coordinates changed + if (currentExif != null && currentExif.hasCoordinates) { + final prevLat = prevExif?.latitude; + final prevLon = prevExif?.longitude; + final currentLat = currentExif.latitude; + final currentLon = currentExif.longitude; + + if (prevLat != currentLat || prevLon != currentLon) { + _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentLat!, currentLon!))); } - }); + } } @override @@ -58,25 +62,28 @@ 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) { + // Watch the providers to ensure widget rebuilds when data changes + final asset = ref.watch(currentAssetNotifier); + final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final hasCoordinates = exifInfo?.hasCoordinates ?? false; - void editLocation() async { - await ref.read(actionProvider.notifier).editLocation(ActionSource.viewer, context); - } - // Guard local assets - if (asset != null && asset is LocalAsset && asset!.hasRemote) { + 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)}"; return Padding( - padding: const EdgeInsets.symmetric(vertical: 12), + padding: const EdgeInsets.only(bottom: 12), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ @@ -93,7 +100,8 @@ class _SheetLocationDetailsState extends ConsumerState { fontWeight: FontWeight.w600, ), ), - if (hasCoordinates) IconButton(onPressed: editLocation, icon: const Icon(Icons.edit), iconSize: 20), + if (hasCoordinates) + IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_location_alt), iconSize: 20), ], ), ), From 4ba7825c2e103ac980de81c1e38e9ca877760714 Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 16 Nov 2025 12:26:01 -0600 Subject: [PATCH 6/9] fix: update currentAssetExifProvider when changing location --- mobile/lib/providers/infrastructure/action.provider.dart | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mobile/lib/providers/infrastructure/action.provider.dart b/mobile/lib/providers/infrastructure/action.provider.dart index 9467f63483f44..e0560a459cb94 100644 --- a/mobile/lib/providers/infrastructure/action.provider.dart +++ b/mobile/lib/providers/infrastructure/action.provider.dart @@ -302,6 +302,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); From 7c024b006b25bfc110e0a70d62cfc5480297f893 Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 16 Nov 2025 12:39:19 -0600 Subject: [PATCH 7/9] chore: use SheetTile for location header --- .../sheet_location_details.widget.dart | 42 +++++++------------ 1 file changed, 15 insertions(+), 27 deletions(-) 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 40a503c785ab9..5ac0817f2096b 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 @@ -87,23 +87,14 @@ class _SheetLocationDetailsState extends ConsumerState { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: context.isMobile ? 16.0 : 56.0), - child: Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Text( - "exif_bottom_sheet_location".t(context: context), - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ), - if (hasCoordinates) - IconButton(onPressed: editLocation, icon: const Icon(Icons.edit_location_alt), iconSize: 20), - ], + 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( @@ -112,7 +103,7 @@ class _SheetLocationDetailsState extends ConsumerState { crossAxisAlignment: CrossAxisAlignment.start, children: [ ExifMap(exifInfo: exifInfo!, markerId: remoteId, onMapCreated: _onMapCreated), - const SizedBox(height: 15), + const SizedBox(height: 16), if (locationName != null) Padding( padding: const EdgeInsets.only(bottom: 4.0), @@ -128,17 +119,14 @@ class _SheetLocationDetailsState extends ConsumerState { ), ), if (!hasCoordinates) - Padding( - padding: const EdgeInsets.only(top: 12), - child: 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, + 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, ), ], ), From fec5ad43e9c2fa19b1e601d8d2ed3852f5efd20e Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 16 Nov 2025 12:42:35 -0600 Subject: [PATCH 8/9] chore: remove coordinate change check --- .../bottom_sheet/sheet_location_details.widget.dart | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) 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 5ac0817f2096b..6ae382c43c90a 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 @@ -40,19 +40,10 @@ class _SheetLocationDetailsState extends ConsumerState { } void _onExifChanged(AsyncValue? previous, AsyncValue current) { - final prevExif = previous?.valueOrNull; final currentExif = current.valueOrNull; - // Update map camera if coordinates changed if (currentExif != null && currentExif.hasCoordinates) { - final prevLat = prevExif?.latitude; - final prevLon = prevExif?.longitude; - final currentLat = currentExif.latitude; - final currentLon = currentExif.longitude; - - if (prevLat != currentLat || prevLon != currentLon) { - _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentLat!, currentLon!))); - } + _mapController?.moveCamera(CameraUpdate.newLatLng(LatLng(currentExif.latitude!, currentExif.longitude!))); } } From 73319f9e057494ce2db9ea43931ea6fbe3893de0 Mon Sep 17 00:00:00 2001 From: bwees Date: Sun, 16 Nov 2025 12:42:59 -0600 Subject: [PATCH 9/9] chore: remove comment --- .../asset_viewer/bottom_sheet/sheet_location_details.widget.dart | 1 - 1 file changed, 1 deletion(-) 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 6ae382c43c90a..05d19476c65e6 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 @@ -59,7 +59,6 @@ class _SheetLocationDetailsState extends ConsumerState { @override Widget build(BuildContext context) { - // Watch the providers to ensure widget rebuilds when data changes final asset = ref.watch(currentAssetNotifier); final exifInfo = ref.watch(currentAssetExifProvider).valueOrNull; final hasCoordinates = exifInfo?.hasCoordinates ?? false;