diff --git a/mobile/assets/i18n/en-US.json b/mobile/assets/i18n/en-US.json index 220a73b58d..47ab78b095 100644 --- a/mobile/assets/i18n/en-US.json +++ b/mobile/assets/i18n/en-US.json @@ -201,7 +201,7 @@ "delete_shared_link_dialog_title": "Delete Shared Link", "description_input_hint_text": "Add description...", "description_input_submit_error": "Error updating description, check the log for more details", - "edit_date_time_dialog_date_time": "Date and Time", + "edit_date_time_dialog_date_time": "Edit date and time", "edit_date_time_dialog_timezone": "Timezone", "edit_location_dialog_title": "Location", "exif_bottom_sheet_description": "Add Description...", diff --git a/mobile/devtools_options.yaml b/mobile/devtools_options.yaml new file mode 100644 index 0000000000..fa0b357c4f --- /dev/null +++ b/mobile/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/mobile/lib/entities/exif_info.entity.dart b/mobile/lib/entities/exif_info.entity.dart index c03c410f69..63d06f5d2c 100644 --- a/mobile/lib/entities/exif_info.entity.dart +++ b/mobile/lib/entities/exif_info.entity.dart @@ -170,6 +170,30 @@ class ExifInfo { state.hashCode ^ country.hashCode ^ description.hashCode; + + @override + String toString() { + return """ +{ + id: $id, + fileSize: $fileSize, + dateTimeOriginal: $dateTimeOriginal, + timeZone: $timeZone, + make: $make, + model: $model, + lens: $lens, + f: $f, + mm: $mm, + iso: $iso, + exposureSeconds: $exposureSeconds, + lat: $lat, + long: $long, + city: $city, + state: $state, + country: $country, + description: $description, +}"""; + } } double? _exposureTimeToSeconds(String? s) { diff --git a/mobile/lib/pages/common/gallery_viewer.page.dart b/mobile/lib/pages/common/gallery_viewer.page.dart index 704ee2829f..93fd5afceb 100644 --- a/mobile/lib/pages/common/gallery_viewer.page.dart +++ b/mobile/lib/pages/common/gallery_viewer.page.dart @@ -22,7 +22,7 @@ import 'package:immich_mobile/services/app_settings.service.dart'; import 'package:immich_mobile/widgets/asset_grid/asset_grid_data_structure.dart'; import 'package:immich_mobile/widgets/asset_viewer/advanced_bottom_sheet.dart'; import 'package:immich_mobile/widgets/asset_viewer/bottom_gallery_bar.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/detail_panel.dart'; import 'package:immich_mobile/widgets/asset_viewer/gallery_app_bar.dart'; import 'package:immich_mobile/widgets/common/immich_image.dart'; import 'package:immich_mobile/widgets/common/immich_thumbnail.dart'; @@ -152,7 +152,7 @@ class GalleryViewerPage extends HookConsumerWidget { .watch(appSettingsServiceProvider) .getSetting(AppSettingsEnum.advancedTroubleshooting) ? AdvancedBottomSheet(assetDetail: asset) - : ExifBottomSheet(asset: asset), + : DetailPanel(asset: asset), ), ); }, diff --git a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart index 5ed85932f8..7f1008c655 100644 --- a/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart +++ b/mobile/lib/pages/sharing/shared_link/shared_link_edit.page.dart @@ -234,14 +234,6 @@ class SharedLinkEditPage extends HookConsumerWidget { onSelected: (value) { expiryAfter.value = value!; }, - inputDecorationTheme: themeData.inputDecorationTheme.copyWith( - disabledBorder: OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey.withOpacity(0.5)), - ), - enabledBorder: const OutlineInputBorder( - borderSide: BorderSide(color: Colors.grey), - ), - ), dropdownMenuEntries: [ DropdownMenuEntry( value: 0, diff --git a/mobile/lib/services/asset.service.dart b/mobile/lib/services/asset.service.dart index 5751c00b47..d37133a63b 100644 --- a/mobile/lib/services/asset.service.dart +++ b/mobile/lib/services/asset.service.dart @@ -162,6 +162,7 @@ class AssetService { final dto = await _apiService.assetsApi.getAssetInfo(a.remoteId!); if (dto != null && dto.exifInfo != null) { final newExif = Asset.remote(dto).exifInfo!.copyWith(id: a.id); + a.exifInfo = newExif; if (newExif != a.exifInfo) { if (a.isInDb) { _db.writeTxn(() => a.put(_db)); diff --git a/mobile/lib/services/asset_description.service.dart b/mobile/lib/services/asset_description.service.dart index 66437d61e2..196e29dc6a 100644 --- a/mobile/lib/services/asset_description.service.dart +++ b/mobile/lib/services/asset_description.service.dart @@ -43,6 +43,19 @@ class AssetDescriptionService { } } } + + String getAssetDescription(Asset asset) { + final localExifId = asset.exifInfo?.id; + + // Guard [remoteAssetId] and [localExifId] null + if (localExifId == null) { + return ""; + } + + final exifInfo = _db.exifInfos.getSync(localExifId); + + return exifInfo?.description ?? ""; + } } final assetDescriptionServiceProvider = Provider( diff --git a/mobile/lib/utils/immich_app_theme.dart b/mobile/lib/utils/immich_app_theme.dart index d61eba73b2..0aac5b476e 100644 --- a/mobile/lib/utils/immich_app_theme.dart +++ b/mobile/lib/utils/immich_app_theme.dart @@ -251,11 +251,13 @@ ThemeData getThemeData({required ColorScheme colorScheme}) { borderSide: BorderSide( color: primaryColor, ), + borderRadius: const BorderRadius.all(Radius.circular(15)), ), enabledBorder: OutlineInputBorder( borderSide: BorderSide( color: colorScheme.outlineVariant, ), + borderRadius: const BorderRadius.all(Radius.circular(15)), ), labelStyle: TextStyle( color: primaryColor, @@ -268,5 +270,34 @@ ThemeData getThemeData({required ColorScheme colorScheme}) { textSelectionTheme: TextSelectionThemeData( cursorColor: primaryColor, ), + dropdownMenuTheme: DropdownMenuThemeData( + menuStyle: MenuStyle( + shape: WidgetStatePropertyAll( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15), + ), + ), + ), + inputDecorationTheme: InputDecorationTheme( + focusedBorder: OutlineInputBorder( + borderSide: BorderSide( + color: primaryColor, + ), + ), + enabledBorder: OutlineInputBorder( + borderSide: BorderSide( + color: colorScheme.outlineVariant, + ), + borderRadius: const BorderRadius.all(Radius.circular(15)), + ), + labelStyle: TextStyle( + color: primaryColor, + ), + hintStyle: const TextStyle( + fontSize: 14.0, + fontWeight: FontWeight.normal, + ), + ), + ), ); } diff --git a/mobile/lib/utils/selection_handlers.dart b/mobile/lib/utils/selection_handlers.dart index 6d11923fd8..56160a2efc 100644 --- a/mobile/lib/utils/selection_handlers.dart +++ b/mobile/lib/utils/selection_handlers.dart @@ -118,6 +118,7 @@ Future handleEditDateTime( initialTZ: timeZone, initialTZOffset: offset, ); + if (dateTime == null) { return; } @@ -142,10 +143,12 @@ Future handleEditLocation( ); } } + final location = await showLocationPicker( context: context, initialLatLng: initialLatLng, ); + if (location == null) { return; } diff --git a/mobile/lib/widgets/asset_viewer/description_input.dart b/mobile/lib/widgets/asset_viewer/description_input.dart index 1a91d1614b..18ef394e2d 100644 --- a/mobile/lib/widgets/asset_viewer/description_input.dart +++ b/mobile/lib/widgets/asset_viewer/description_input.dart @@ -6,6 +6,7 @@ import 'package:immich_mobile/entities/exif_info.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/theme_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; import 'package:immich_mobile/providers/user.provider.dart'; import 'package:immich_mobile/services/asset_description.service.dart'; import 'package:immich_mobile/widgets/common/immich_toast.dart'; @@ -29,17 +30,16 @@ class DescriptionInput extends HookConsumerWidget { final isFocus = useState(false); final isTextEmpty = useState(controller.text.isEmpty); final descriptionProvider = ref.watch(assetDescriptionServiceProvider); - final owner = ref.watch(currentUserProvider); final hasError = useState(false); + final assetWithExif = ref.watch(assetDetailProvider(asset)); useEffect( () { - controller.text = exifInfo?.description ?? ''; - isTextEmpty.value = exifInfo?.description?.isEmpty ?? true; + controller.text = descriptionProvider.getAssetDescription(asset); return null; }, - [exifInfo?.description], + [assetWithExif.value], ); submitDescription(String description) async { @@ -49,6 +49,7 @@ class DescriptionInput extends HookConsumerWidget { asset, description, ); + controller.text = description; } catch (error, stack) { hasError.value = true; _log.severe("Error updating description", error, stack); @@ -101,6 +102,11 @@ class DescriptionInput extends HookConsumerWidget { hintText: 'description_input_hint_text'.tr(), border: InputBorder.none, suffixIcon: suffixIcon, + enabledBorder: InputBorder.none, + focusedBorder: InputBorder.none, + disabledBorder: InputBorder.none, + errorBorder: InputBorder.none, + focusedErrorBorder: InputBorder.none, ), ); } diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart new file mode 100644 index 0000000000..e29da52280 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_date_time.dart @@ -0,0 +1,54 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/extensions/asset_extensions.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/extensions/duration_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/utils/selection_handlers.dart'; + +class AssetDateTime extends ConsumerWidget { + final Asset asset; + + const AssetDateTime({super.key, required this.asset}); + + String getDateTimeString(Asset a) { + final (deltaTime, timeZone) = a.getTZAdjustedTimeAndOffset(); + final date = DateFormat.yMMMEd().format(deltaTime); + final time = DateFormat.jm().format(deltaTime); + return '$date • $time GMT${timeZone.formatAsOffset()}'; + } + + @override + Widget build(BuildContext context, WidgetRef ref) { + final watchedAsset = ref.watch(assetDetailProvider(asset)); + String formattedDateTime = getDateTimeString(asset); + + void editDateTime() async { + await handleEditDateTime(ref, context, [asset]); + + if (watchedAsset.value != null) { + formattedDateTime = getDateTimeString(watchedAsset.value!); + } + } + + return Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + formattedDateTime, + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + if (asset.isRemote) + IconButton( + onPressed: editDateTime, + icon: const Icon(Icons.edit_outlined), + iconSize: 20, + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart new file mode 100644 index 0000000000..a78a309512 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_details.dart @@ -0,0 +1,44 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/file_info.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/camera_info.dart'; + +class AssetDetails extends ConsumerWidget { + final Asset asset; + final ExifInfo? exifInfo; + + const AssetDetails({ + super.key, + required this.asset, + this.exifInfo, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetWithExif = ref.watch(assetDetailProvider(asset)); + final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; + + return Padding( + padding: const EdgeInsets.only(top: 24.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + "exif_bottom_sheet_details", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + FileInfo(asset: asset), + if (exifInfo?.make != null) CameraInfo(exifInfo: exifInfo!), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart new file mode 100644 index 0000000000..364b568d0a --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/asset_location.dart @@ -0,0 +1,106 @@ +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset.provider.dart'; +import 'package:immich_mobile/utils/selection_handlers.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/exif_map.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; + +class AssetLocation extends HookConsumerWidget { + final Asset asset; + + const AssetLocation({ + super.key, + required this.asset, + }); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final assetWithExif = ref.watch(assetDetailProvider(asset)); + final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; + final hasCoordinates = exifInfo?.hasCoordinates ?? false; + + void editLocation() { + handleEditLocation(ref, context, [assetWithExif.value ?? asset]); + } + + // Guard no lat/lng + if (!hasCoordinates) { + return asset.isRemote + ? ListTile( + minLeadingWidth: 0, + contentPadding: const EdgeInsets.all(0), + leading: const Icon(Icons.location_on), + title: Text( + "exif_bottom_sheet_location_add", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + onTap: editLocation, + ) + : const SizedBox.shrink(); + } + + Widget getLocationName() { + if (exifInfo == null) { + return const SizedBox.shrink(); + } + + final cityName = exifInfo.city; + final stateName = exifInfo.state; + + bool hasLocationName = (cityName != null && stateName != null); + + return hasLocationName + ? Text( + "$cityName, $stateName", + style: context.textTheme.labelLarge, + ) + : const SizedBox.shrink(); + } + + return Padding( + padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Text( + "exif_bottom_sheet_location", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + if (asset.isRemote) + IconButton( + onPressed: editLocation, + icon: const Icon(Icons.edit_outlined), + iconSize: 20, + ), + ], + ), + asset.isRemote ? const SizedBox.shrink() : const SizedBox(height: 16), + ExifMap( + exifInfo: exifInfo!, + markerId: asset.remoteId, + ), + const SizedBox(height: 16), + getLocationName(), + Text( + "${exifInfo.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(150), + ), + ), + ], + ), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart new file mode 100644 index 0000000000..e6720e0255 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/camera_info.dart @@ -0,0 +1,38 @@ +import 'package:flutter/material.dart'; +import 'package:immich_mobile/entities/exif_info.entity.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; + +class CameraInfo extends StatelessWidget { + final ExifInfo exifInfo; + + const CameraInfo({ + super.key, + required this.exifInfo, + }); + + @override + Widget build(BuildContext context) { + final textColor = context.isDarkTheme ? Colors.white : Colors.black; + return ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: Icon( + Icons.camera, + color: textColor.withAlpha(200), + ), + title: Text( + "${exifInfo.make} ${exifInfo.model}", + style: context.textTheme.labelLarge, + ), + subtitle: exifInfo.f != null || + exifInfo.exposureSeconds != null || + exifInfo.mm != null || + exifInfo.iso != null + ? Text( + "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", + style: context.textTheme.bodySmall, + ) + : null, + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart new file mode 100644 index 0000000000..db9dafebcb --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/detail_panel.dart @@ -0,0 +1,37 @@ +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_date_time.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_details.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/asset_location.dart'; +import 'package:immich_mobile/widgets/asset_viewer/detail_panel/people_info.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; + +class DetailPanel extends HookConsumerWidget { + final Asset asset; + + const DetailPanel({super.key, required this.asset}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + return ListView( + shrinkWrap: true, + children: [ + Padding( + padding: const EdgeInsets.symmetric(horizontal: 16.0), + child: Column( + children: [ + AssetDateTime(asset: asset), + asset.isRemote + ? DescriptionInput(asset: asset) + : const SizedBox.shrink(), + PeopleInfo(asset: asset), + AssetLocation(asset: asset), + AssetDetails(asset: asset), + ], + ), + ), + ], + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_map.dart b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart similarity index 63% rename from mobile/lib/widgets/asset_viewer/exif_sheet/exif_map.dart rename to mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart index a2a78b103c..7878404273 100644 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_map.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/exif_map.dart @@ -8,13 +8,11 @@ import 'package:url_launcher/url_launcher.dart'; class ExifMap extends StatelessWidget { final ExifInfo exifInfo; - final String formattedDateTime; final String? markerId; const ExifMap({ super.key, required this.exifInfo, - required this.formattedDateTime, this.markerId = 'marker', }); @@ -37,7 +35,7 @@ class ExifMap extends StatelessWidget { host: '$latitude,$longitude', queryParameters: { 'z': '$zoomLevel', - 'q': '$latitude,$longitude($formattedDateTime)', + 'q': '$latitude,$longitude', }, ); if (await canLaunchUrl(uri)) { @@ -46,7 +44,7 @@ class ExifMap extends StatelessWidget { } else if (Platform.isIOS) { var params = { 'll': '$latitude,$longitude', - 'q': formattedDateTime, + 'q': '$latitude,$longitude', 'z': '$zoomLevel', }; Uri uri = Uri.https('maps.apple.com', '/', params); @@ -63,32 +61,29 @@ class ExifMap extends StatelessWidget { ); } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: LayoutBuilder( - builder: (context, constraints) { - return MapThumbnail( - centre: LatLng( - exifInfo.latitude ?? 0, - exifInfo.longitude ?? 0, - ), - height: 150, - width: constraints.maxWidth, - zoom: 12.0, - assetMarkerRemoteId: markerId, - onTap: (tapPosition, latLong) async { - Uri? uri = await createCoordinatesUri(); + return LayoutBuilder( + builder: (context, constraints) { + return MapThumbnail( + centre: LatLng( + exifInfo.latitude ?? 0, + exifInfo.longitude ?? 0, + ), + height: 150, + width: constraints.maxWidth, + zoom: 12.0, + assetMarkerRemoteId: markerId, + onTap: (tapPosition, latLong) async { + Uri? uri = await createCoordinatesUri(); - if (uri == null) { - return; - } + if (uri == null) { + return; + } - debugPrint('Opening Map Uri: $uri'); - launchUrl(uri); - }, - ); - }, - ), + debugPrint('Opening Map Uri: $uri'); + launchUrl(uri); + }, + ); + }, ); } } diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_image_properties.dart b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart similarity index 95% rename from mobile/lib/widgets/asset_viewer/exif_sheet/exif_image_properties.dart rename to mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart index 6f268c3d71..3c650bdc6a 100644 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_image_properties.dart +++ b/mobile/lib/widgets/asset_viewer/detail_panel/file_info.dart @@ -3,10 +3,10 @@ import 'package:immich_mobile/entities/asset.entity.dart'; import 'package:immich_mobile/extensions/build_context_extensions.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; -class ExifImageProperties extends StatelessWidget { +class FileInfo extends StatelessWidget { final Asset asset; - const ExifImageProperties({ + const FileInfo({ super.key, required this.asset, }); diff --git a/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart new file mode 100644 index 0000000000..f917f03b37 --- /dev/null +++ b/mobile/lib/widgets/asset_viewer/detail_panel/people_info.dart @@ -0,0 +1,102 @@ +import 'dart:math' as math; + +import 'package:auto_route/auto_route.dart'; +import 'package:easy_localization/easy_localization.dart'; +import 'package:flutter/material.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; +import 'package:immich_mobile/extensions/build_context_extensions.dart'; +import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; +import 'package:immich_mobile/models/search/search_curated_content.model.dart'; +import 'package:immich_mobile/widgets/search/curated_people_row.dart'; +import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; +import 'package:immich_mobile/routing/router.dart'; +import 'package:immich_mobile/entities/asset.entity.dart'; + +class PeopleInfo extends ConsumerWidget { + final Asset asset; + final EdgeInsets? padding; + + const PeopleInfo({super.key, required this.asset, this.padding}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final peopleProvider = + ref.watch(assetPeopleNotifierProvider(asset).notifier); + final people = ref + .watch(assetPeopleNotifierProvider(asset)) + .value + ?.where((p) => !p.isHidden); + final double imageSize = math.min(context.width / 3, 150); + + showPersonNameEditModel( + String personId, + String personName, + ) { + return showDialog( + context: context, + builder: (BuildContext context) { + return PersonNameEditForm(personId: personId, personName: personName); + }, + ).then((_) { + // ensure the people list is up-to-date. + peopleProvider.refresh(); + }); + } + + final curatedPeople = people + ?.map((p) => SearchCuratedContent(id: p.id, label: p.name)) + .toList() ?? + []; + + return AnimatedCrossFade( + crossFadeState: (people?.isEmpty ?? true) + ? CrossFadeState.showFirst + : CrossFadeState.showSecond, + duration: const Duration(milliseconds: 200), + firstChild: Container(), + secondChild: Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Column( + children: [ + Padding( + padding: padding ?? EdgeInsets.zero, + child: Align( + alignment: Alignment.topLeft, + child: Text( + "exif_bottom_sheet_people", + style: context.textTheme.labelMedium?.copyWith( + color: context.textTheme.labelMedium?.color?.withAlpha(200), + fontWeight: FontWeight.w600, + ), + ).tr(), + ), + ), + SizedBox( + height: imageSize, + child: Padding( + padding: const EdgeInsets.only(top: 16.0), + child: CuratedPeopleRow( + padding: padding, + content: curatedPeople, + onTap: (content, index) { + context + .pushRoute( + PersonResultRoute( + personId: content.id, + personName: content.label, + ), + ) + .then((_) => peopleProvider.refresh()); + }, + onNameTap: (person, index) => { + showPersonNameEditModel(person.id, person.label), + }, + ), + ), + ), + ], + ), + ), + ); + } +} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart deleted file mode 100644 index ae32c133c3..0000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_bottom_sheet.dart +++ /dev/null @@ -1,212 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/asset_extensions.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/extensions/duration_extensions.dart'; -import 'package:immich_mobile/widgets/asset_viewer/description_input.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_detail.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_image_properties.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_location.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_people.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; -import 'package:immich_mobile/providers/asset.provider.dart'; -import 'package:immich_mobile/utils/selection_handlers.dart'; - -class ExifBottomSheet extends HookConsumerWidget { - final Asset asset; - - const ExifBottomSheet({super.key, required this.asset}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final assetWithExif = ref.watch(assetDetailProvider(asset)); - var textColor = context.colorScheme.onSurface; - final ExifInfo? exifInfo = (assetWithExif.value ?? asset).exifInfo; - // Format the date time with the timezone - final (dt, timeZone) = - (assetWithExif.value ?? asset).getTZAdjustedTimeAndOffset(); - final date = DateFormat.yMMMEd().format(dt); - final time = DateFormat.jm().format(dt); - - String formattedDateTime = '$date • $time GMT${timeZone.formatAsOffset()}'; - - final dateWidget = Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - formattedDateTime, - style: const TextStyle( - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - if (asset.isRemote) - IconButton( - onPressed: () => handleEditDateTime( - ref, - context, - [assetWithExif.value ?? asset], - ), - icon: const Icon(Icons.edit_outlined), - iconSize: 20, - ), - ], - ); - - return SingleChildScrollView( - padding: const EdgeInsets.only( - bottom: 50, - ), - child: LayoutBuilder( - builder: (context, constraints) { - final horizontalPadding = constraints.maxWidth > 600 ? 24.0 : 16.0; - if (constraints.maxWidth > 600) { - // Two column - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), - child: Column( - children: [ - dateWidget, - if (asset.isRemote) - DescriptionInput(asset: asset, exifInfo: exifInfo), - ], - ), - ), - ExifPeople( - asset: asset, - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - ), - Padding( - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - child: Row( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Expanded( - child: Padding( - padding: const EdgeInsets.only(right: 8.0), - child: ExifLocation( - asset: asset, - exifInfo: exifInfo, - editLocation: () => handleEditLocation( - ref, - context, - [assetWithExif.value ?? asset], - ), - formattedDateTime: formattedDateTime, - ), - ), - ), - ConstrainedBox( - constraints: const BoxConstraints(maxWidth: 300), - child: Padding( - padding: const EdgeInsets.only(left: 8.0), - child: ExifDetail(asset: asset, exifInfo: exifInfo), - ), - ), - ], - ), - ), - ], - ); - } - - // One column - return Column( - mainAxisSize: MainAxisSize.min, - crossAxisAlignment: CrossAxisAlignment.stretch, - children: [ - Padding( - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - child: Column( - children: [ - dateWidget, - if (asset.isRemote) - DescriptionInput(asset: asset, exifInfo: exifInfo), - Padding( - padding: EdgeInsets.only(top: asset.isRemote ? 0 : 16.0), - child: ExifLocation( - asset: asset, - exifInfo: exifInfo, - editLocation: () => handleEditLocation( - ref, - context, - [assetWithExif.value ?? asset], - ), - formattedDateTime: formattedDateTime, - ), - ), - ], - ), - ), - Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0), - child: ExifPeople( - asset: asset, - padding: EdgeInsets.symmetric( - horizontal: horizontalPadding, - ), - ), - ), - Padding( - padding: EdgeInsets.symmetric(horizontal: horizontalPadding), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color - ?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ExifImageProperties(asset: asset), - if (exifInfo?.make != null) - ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon( - Icons.camera, - color: textColor.withAlpha(200), - ), - title: Text( - "${exifInfo!.make} ${exifInfo.model}", - style: context.textTheme.labelLarge, - ), - subtitle: exifInfo.f != null || - exifInfo.exposureSeconds != null || - exifInfo.mm != null || - exifInfo.iso != null - ? Text( - "ƒ/${exifInfo.fNumber} ${exifInfo.exposureTime} ${exifInfo.focalLength} mm ISO ${exifInfo.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ), - ], - ), - ), - const SizedBox(height: 50), - ], - ); - }, - ), - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_detail.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_detail.dart deleted file mode 100644 index acd0d2d202..0000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_detail.dart +++ /dev/null @@ -1,60 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_image_properties.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; - -class ExifDetail extends StatelessWidget { - final Asset asset; - final ExifInfo? exifInfo; - - const ExifDetail({ - super.key, - required this.asset, - this.exifInfo, - }); - - @override - Widget build(BuildContext context) { - final textColor = context.isDarkTheme ? Colors.white : Colors.black; - return Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "exif_bottom_sheet_details", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ExifImageProperties(asset: asset), - if (exifInfo?.make != null) - ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - leading: Icon( - Icons.camera, - color: textColor.withAlpha(200), - ), - title: Text( - "${exifInfo?.make} ${exifInfo?.model}", - style: context.textTheme.labelLarge, - ), - subtitle: exifInfo?.f != null || - exifInfo?.exposureSeconds != null || - exifInfo?.mm != null || - exifInfo?.iso != null - ? Text( - "ƒ/${exifInfo?.fNumber} ${exifInfo?.exposureTime} ${exifInfo?.focalLength} mm ISO ${exifInfo?.iso ?? ''} ", - style: context.textTheme.bodySmall, - ) - : null, - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_location.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_location.dart deleted file mode 100644 index 713a75c06e..0000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_location.dart +++ /dev/null @@ -1,105 +0,0 @@ -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/widgets/asset_viewer/exif_sheet/exif_map.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; -import 'package:immich_mobile/entities/exif_info.entity.dart'; - -class ExifLocation extends StatelessWidget { - final Asset asset; - final ExifInfo? exifInfo; - final void Function() editLocation; - final String formattedDateTime; - - const ExifLocation({ - super.key, - required this.asset, - required this.exifInfo, - required this.editLocation, - required this.formattedDateTime, - }); - - @override - Widget build(BuildContext context) { - final hasCoordinates = exifInfo?.hasCoordinates ?? false; - // Guard no lat/lng - if (!hasCoordinates) { - return asset.isRemote - ? ListTile( - minLeadingWidth: 0, - contentPadding: const EdgeInsets.all(0), - leading: const Icon(Icons.location_on), - title: Text( - "exif_bottom_sheet_location_add", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - onTap: editLocation, - ) - : const SizedBox.shrink(); - } - - return Column( - children: [ - // Location - Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - mainAxisAlignment: MainAxisAlignment.spaceBetween, - children: [ - Text( - "exif_bottom_sheet_location", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - if (asset.isRemote) - IconButton( - onPressed: editLocation, - icon: const Icon(Icons.edit_outlined), - iconSize: 20, - ), - ], - ), - ExifMap( - exifInfo: exifInfo!, - formattedDateTime: formattedDateTime, - markerId: asset.remoteId, - ), - RichText( - text: TextSpan( - style: context.textTheme.labelLarge, - children: [ - if (exifInfo != null && exifInfo?.city != null) - TextSpan( - text: exifInfo!.city, - ), - if (exifInfo != null && - exifInfo?.city != null && - exifInfo?.state != null) - const TextSpan( - text: ", ", - ), - if (exifInfo != null && exifInfo?.state != null) - TextSpan( - text: exifInfo!.state, - ), - ], - ), - ), - Text( - "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo!.longitude!.toStringAsFixed(4)}", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(150), - ), - ), - ], - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_people.dart b/mobile/lib/widgets/asset_viewer/exif_sheet/exif_people.dart deleted file mode 100644 index 532a74dd2a..0000000000 --- a/mobile/lib/widgets/asset_viewer/exif_sheet/exif_people.dart +++ /dev/null @@ -1,97 +0,0 @@ -import 'dart:math' as math; - -import 'package:auto_route/auto_route.dart'; -import 'package:easy_localization/easy_localization.dart'; -import 'package:flutter/material.dart'; -import 'package:hooks_riverpod/hooks_riverpod.dart'; -import 'package:immich_mobile/extensions/build_context_extensions.dart'; -import 'package:immich_mobile/providers/asset_viewer/asset_people.provider.dart'; -import 'package:immich_mobile/models/search/search_curated_content.model.dart'; -import 'package:immich_mobile/widgets/search/curated_people_row.dart'; -import 'package:immich_mobile/widgets/search/person_name_edit_form.dart'; -import 'package:immich_mobile/routing/router.dart'; -import 'package:immich_mobile/entities/asset.entity.dart'; - -class ExifPeople extends ConsumerWidget { - final Asset asset; - final EdgeInsets? padding; - - const ExifPeople({super.key, required this.asset, this.padding}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final peopleProvider = - ref.watch(assetPeopleNotifierProvider(asset).notifier); - final people = ref - .watch(assetPeopleNotifierProvider(asset)) - .value - ?.where((p) => !p.isHidden); - final double imageSize = math.min(context.width / 3, 150); - - showPersonNameEditModel( - String personId, - String personName, - ) { - return showDialog( - context: context, - builder: (BuildContext context) { - return PersonNameEditForm(personId: personId, personName: personName); - }, - ).then((_) { - // ensure the people list is up-to-date. - peopleProvider.refresh(); - }); - } - - if (people?.isEmpty ?? true) { - // Empty list or loading - return Container(); - } - - final curatedPeople = people - ?.map((p) => SearchCuratedContent(id: p.id, label: p.name)) - .toList() ?? - []; - - return Column( - children: [ - Padding( - padding: padding ?? EdgeInsets.zero, - child: Align( - alignment: Alignment.topLeft, - child: Text( - "exif_bottom_sheet_people", - style: context.textTheme.labelMedium?.copyWith( - color: context.textTheme.labelMedium?.color?.withAlpha(200), - fontWeight: FontWeight.w600, - ), - ).tr(), - ), - ), - SizedBox( - height: imageSize, - child: Padding( - padding: const EdgeInsets.only(top: 8.0), - child: CuratedPeopleRow( - padding: padding, - content: curatedPeople, - onTap: (content, index) { - context - .pushRoute( - PersonResultRoute( - personId: content.id, - personName: content.label, - ), - ) - .then((_) => peopleProvider.refresh()); - }, - onNameTap: (person, index) => { - showPersonNameEditModel(person.id, person.label), - }, - ), - ), - ), - ], - ); - } -} diff --git a/mobile/lib/widgets/common/date_time_picker.dart b/mobile/lib/widgets/common/date_time_picker.dart index 746917d3fb..d90ee40e47 100644 --- a/mobile/lib/widgets/common/date_time_picker.dart +++ b/mobile/lib/widgets/common/date_time_picker.dart @@ -84,6 +84,19 @@ class _DateTimePicker extends HookWidget { final date = useState(initialDateTime ?? DateTime.now()); final tzOffset = useState<_TimeZoneOffset>(_getInitiationLocation()); final timeZones = useMemoized(() => getAllTimeZones(), const []); + final menuEntries = timeZones + .map( + (timezone) => DropdownMenuEntry<_TimeZoneOffset>( + value: timezone, + label: timezone.display, + style: ButtonStyle( + textStyle: WidgetStatePropertyAll( + context.textTheme.bodyMedium, + ), + ), + ), + ) + .toList(); void pickDate() async { final now = DateTime.now(); @@ -120,93 +133,84 @@ class _DateTimePicker extends HookWidget { context.pop(dtWithOffset); } - return AlertDialog( - contentPadding: const EdgeInsets.all(30), - alignment: Alignment.center, - content: Column( - mainAxisSize: MainAxisSize.min, - children: [ - const Text( - "edit_date_time_dialog_date_time", - textAlign: TextAlign.center, - ).tr(), - TextButton.icon( - onPressed: pickDate, - icon: Text( - DateFormat("dd-MM-yyyy hh:mm a").format(date.value), - style: context.textTheme.bodyLarge - ?.copyWith(color: context.primaryColor), - ), - label: const Icon( - Icons.edit_outlined, - size: 18, - ), + return LayoutBuilder( + builder: (context, constraint) => AlertDialog( + contentPadding: + const EdgeInsets.symmetric(vertical: 32, horizontal: 18), + actions: [ + TextButton( + onPressed: () => context.pop(), + child: Text( + "action_common_cancel", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.colorScheme.error, + ), + ).tr(), ), - const Text( - "edit_date_time_dialog_timezone", - textAlign: TextAlign.center, - ).tr(), - DropdownMenu( - menuHeight: 300, - width: 280, - inputDecorationTheme: const InputDecorationTheme( - border: InputBorder.none, - contentPadding: EdgeInsets.zero, + TextButton( + onPressed: popWithDateTime, + child: Text( + "action_common_update", + style: context.textTheme.bodyMedium?.copyWith( + fontWeight: FontWeight.w600, + color: context.primaryColor, + ), + ).tr(), + ), + ], + content: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Text( + "edit_date_time_dialog_date_time", + style: TextStyle( + fontSize: 16, + fontWeight: FontWeight.w600, + ), + ).tr(), + const SizedBox(height: 32), + ListTile( + tileColor: context.colorScheme.surfaceContainerHighest, + shape: ShapeBorder.lerp( + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + 1, + ), + trailing: Icon( + Icons.edit_outlined, + size: 18, + color: context.primaryColor, + ), + title: Text( + DateFormat("dd-MM-yyyy hh:mm a").format(date.value), + style: context.textTheme.bodyMedium, + ).tr(), + onTap: pickDate, ), - trailingIcon: Padding( - padding: const EdgeInsets.only(right: 10), - child: Icon( + const SizedBox(height: 24), + DropdownMenu( + width: 275, + menuHeight: 300, + trailingIcon: Icon( Icons.arrow_drop_down, color: context.primaryColor, ), + hintText: "edit_date_time_dialog_timezone".tr(), + label: const Text('edit_date_time_dialog_timezone').tr(), + textStyle: context.textTheme.bodyMedium, + onSelected: (value) => tzOffset.value = value!, + initialSelection: tzOffset.value, + dropdownMenuEntries: menuEntries, ), - textStyle: context.textTheme.bodyLarge?.copyWith( - color: context.primaryColor, - ), - menuStyle: const MenuStyle( - fixedSize: WidgetStatePropertyAll(Size.fromWidth(350)), - alignment: Alignment(-1.25, 0.5), - ), - onSelected: (value) => tzOffset.value = value!, - initialSelection: tzOffset.value, - dropdownMenuEntries: timeZones - .map( - (t) => DropdownMenuEntry<_TimeZoneOffset>( - value: t, - label: t.display, - style: ButtonStyle( - textStyle: WidgetStatePropertyAll( - context.textTheme.bodyMedium, - ), - ), - ), - ) - .toList(), - ), - ], + ], + ), ), - 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: popWithDateTime, - child: Text( - "action_common_update", - style: context.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - color: context.primaryColor, - ), - ).tr(), - ), - ], ); } } diff --git a/mobile/lib/widgets/common/immich_app_bar.dart b/mobile/lib/widgets/common/immich_app_bar.dart index 30802a435a..455a19fcdb 100644 --- a/mobile/lib/widgets/common/immich_app_bar.dart +++ b/mobile/lib/widgets/common/immich_app_bar.dart @@ -65,8 +65,8 @@ class ImmichAppBar extends ConsumerWidget implements PreferredSizeWidget { size: widgetSize, ) : UserCircleAvatar( - radius: 15, - size: 27, + radius: 17, + size: 31, user: user, ), ), diff --git a/mobile/lib/widgets/search/search_filter/common/dropdown.dart b/mobile/lib/widgets/search/search_filter/common/dropdown.dart index 55b54ce46a..230d7dd4da 100644 --- a/mobile/lib/widgets/search/search_filter/common/dropdown.dart +++ b/mobile/lib/widgets/search/search_filter/common/dropdown.dart @@ -18,13 +18,6 @@ class SearchDropdown extends StatelessWidget { @override Widget build(BuildContext context) { - final inputDecorationTheme = InputDecorationTheme( - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(20), - ), - contentPadding: const EdgeInsets.only(left: 16), - ); - final menuStyle = MenuStyle( shape: WidgetStatePropertyAll( RoundedRectangleBorder( @@ -40,7 +33,6 @@ class SearchDropdown extends StatelessWidget { width: constraints.maxWidth, dropdownMenuEntries: dropdownMenuEntries, label: label, - inputDecorationTheme: inputDecorationTheme, menuStyle: menuStyle, trailingIcon: const Icon(Icons.arrow_drop_down_rounded), selectedTrailingIcon: const Icon(Icons.arrow_drop_up_rounded),