diff --git a/mobile/flutter_01.png b/mobile/flutter_01.png new file mode 100644 index 0000000000000..e496e25ffb627 Binary files /dev/null and b/mobile/flutter_01.png differ diff --git a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart index 23dd1ccb0d1b6..ec66387e7a18c 100644 --- a/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart +++ b/mobile/lib/modules/asset_viewer/ui/exif_bottom_sheet.dart @@ -3,12 +3,13 @@ import 'package:flutter/material.dart'; import 'package:flutter_map/flutter_map.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/shared/models/asset.dart'; +import 'package:immich_mobile/shared/ui/drag_sheet.dart'; import 'package:openapi/api.dart'; import 'package:path/path.dart' as p; import 'package:latlong2/latlong.dart'; import 'package:immich_mobile/utils/bytes_units.dart'; -class ExifBottomSheet extends ConsumerWidget { +class ExifBottomSheet extends HookConsumerWidget { final Asset assetDetail; const ExifBottomSheet({Key? key, required this.assetDetail}) @@ -65,6 +66,8 @@ class ExifBottomSheet extends ConsumerWidget { ); } + final textColor = Theme.of(context).primaryColor; + ExifResponseDto? exifInfo = assetDetail.remote?.exifInfo; buildLocationText() { @@ -72,120 +75,125 @@ class ExifBottomSheet extends ConsumerWidget { "${exifInfo?.city}, ${exifInfo?.state}", style: TextStyle( fontSize: 12, - color: Colors.grey[200], fontWeight: FontWeight.bold, + color: textColor, ), ); } - return Padding( - padding: const EdgeInsets.symmetric(vertical: 16.0, horizontal: 8), - child: ListView( - children: [ - if (exifInfo?.dateTimeOriginal != null) - Text( - DateFormat('date_format'.tr()).format( - exifInfo!.dateTimeOriginal!.toLocal(), - ), - style: TextStyle( - color: Colors.grey[400], - fontWeight: FontWeight.bold, - fontSize: 14, - ), - ), - Padding( - padding: const EdgeInsets.only(top: 16.0), - child: Text( - "exif_bottom_sheet_description", - style: TextStyle( - color: Colors.grey[500], - fontSize: 11, - ), - ).tr(), - ), - - // Location - if (assetDetail.latitude != null) - Padding( - padding: const EdgeInsets.only(top: 32.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Divider( - thickness: 1, - color: Colors.grey[600], - ), - Text( - "exif_bottom_sheet_location", - style: TextStyle(fontSize: 11, color: Colors.grey[400]), - ).tr(), - if (assetDetail.latitude != null && - assetDetail.longitude != null) - buildMap(), - if (exifInfo != null && - exifInfo.city != null && - exifInfo.state != null) - buildLocationText(), - Text( - "${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}", - style: TextStyle(fontSize: 12, color: Colors.grey[400]), - ) - ], + return SingleChildScrollView( + child: Card( + margin: const EdgeInsets.all(0), + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const SizedBox(height: 12), + const Align( + alignment: Alignment.center, + child: CustomDraggingHandle(), ), - ), - // Detail - if (exifInfo != null) - Padding( - padding: const EdgeInsets.only(top: 32.0), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Divider( - thickness: 1, - color: Colors.grey[600], + const SizedBox(height: 12), + if (exifInfo?.dateTimeOriginal != null) + Text( + DateFormat('date_format'.tr()).format( + exifInfo!.dateTimeOriginal!.toLocal(), ), - Padding( - padding: const EdgeInsets.only(bottom: 8.0), - child: Text( - "exif_bottom_sheet_details", - style: TextStyle(fontSize: 11, color: Colors.grey[400]), - ).tr(), + style: const TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, ), - ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - textColor: Colors.grey[300], - iconColor: Colors.grey[300], - leading: const Icon(Icons.image), - title: Text( - "${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}", - style: const TextStyle(fontWeight: FontWeight.bold), - ), - subtitle: exifInfo.exifImageHeight != null - ? Text( - "${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${formatBytes(exifInfo.fileSizeInByte!)} ", - ) - : null, + ), + + // Location + if (assetDetail.latitude != null) + Padding( + padding: const EdgeInsets.only(top: 32.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + const Divider( + thickness: 1, + ), + Text( + "exif_bottom_sheet_location", + style: TextStyle(fontSize: 11, color: textColor), + ).tr(), + if (assetDetail.latitude != null && + assetDetail.longitude != null) + buildMap(), + if (exifInfo != null && + exifInfo.city != null && + exifInfo.state != null) + buildLocationText(), + Text( + "${assetDetail.latitude?.toStringAsFixed(4)}, ${assetDetail.longitude?.toStringAsFixed(4)}", + style: const TextStyle(fontSize: 12), + ) + ], ), - if (exifInfo.make != null) - ListTile( - contentPadding: const EdgeInsets.all(0), - dense: true, - textColor: Colors.grey[300], - iconColor: Colors.grey[300], - leading: const Icon(Icons.camera), - title: Text( - "${exifInfo.make} ${exifInfo.model}", - style: const TextStyle(fontWeight: FontWeight.bold), + ), + // Detail + if (exifInfo != null) + Padding( + padding: const EdgeInsets.only(top: 32.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Divider( + thickness: 1, + color: Colors.grey[600], ), - subtitle: Text( - "ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ", + Padding( + padding: const EdgeInsets.only(bottom: 8.0), + child: Text( + "exif_bottom_sheet_details", + style: TextStyle(fontSize: 11, color: textColor), + ).tr(), ), - ), - ], + ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: const Icon(Icons.image), + title: Text( + "${exifInfo.imageName!}${p.extension(assetDetail.remote!.originalPath)}", + style: TextStyle( + fontWeight: FontWeight.bold, + color: textColor, + ), + ), + subtitle: exifInfo.exifImageHeight != null + ? Text( + "${exifInfo.exifImageHeight} x ${exifInfo.exifImageWidth} ${formatBytes(exifInfo.fileSizeInByte ?? 0)} ", + ) + : null, + ), + if (exifInfo.make != null) + ListTile( + contentPadding: const EdgeInsets.all(0), + dense: true, + leading: const Icon(Icons.camera), + title: Text( + "${exifInfo.make} ${exifInfo.model}", + style: TextStyle( + color: textColor, + fontWeight: FontWeight.bold, + ), + ), + subtitle: Text( + "ƒ/${exifInfo.fNumber} 1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)} ${exifInfo.focalLength} mm ISO${exifInfo.iso} ", + ), + ), + ], + ), + ), + const SizedBox( + height: 50, ), - ), - ], + ], + ), + ), ), ); } diff --git a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart index 0991846cb7cec..000d85c699ea0 100644 --- a/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart +++ b/mobile/lib/modules/asset_viewer/views/gallery_viewer.dart @@ -69,9 +69,12 @@ class GalleryViewerPage extends HookConsumerWidget { void showInfo() { showModalBottomSheet( - backgroundColor: Colors.black, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(15.0), + ), barrierColor: Colors.transparent, - isScrollControlled: false, + backgroundColor: Colors.transparent, + isScrollControlled: true, context: context, builder: (context) { return ExifBottomSheet(assetDetail: assetDetail!); @@ -162,6 +165,7 @@ class GalleryViewerPage extends HookConsumerWidget { heroTag: assetList[index].id, loadPreview: isLoadPreview.value, loadOriginal: isLoadOriginal.value, + showExifSheet: showInfo, ); } } else { diff --git a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart index e181ad4cd3433..c2368dfc10109 100644 --- a/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart +++ b/mobile/lib/modules/asset_viewer/views/image_viewer_page.dart @@ -4,7 +4,6 @@ import 'package:flutter_hooks/flutter_hooks.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/modules/asset_viewer/models/image_viewer_page_state.model.dart'; import 'package:immich_mobile/modules/asset_viewer/providers/image_viewer_page_state.provider.dart'; -import 'package:immich_mobile/modules/asset_viewer/ui/exif_bottom_sheet.dart'; import 'package:immich_mobile/modules/asset_viewer/ui/remote_photo_view.dart'; import 'package:immich_mobile/modules/home/services/asset.service.dart'; import 'package:immich_mobile/shared/models/asset.dart'; @@ -17,6 +16,7 @@ class ImageViewerPage extends HookConsumerWidget { final String authToken; final ValueNotifier isZoomedListener; final void Function() isZoomedFunction; + final void Function()? showExifSheet; final bool loadPreview; final bool loadOriginal; @@ -29,6 +29,7 @@ class ImageViewerPage extends HookConsumerWidget { required this.isZoomedListener, required this.loadPreview, required this.loadOriginal, + this.showExifSheet, }) : super(key: key); Asset? assetDetail; @@ -56,18 +57,6 @@ class ImageViewerPage extends HookConsumerWidget { [], ); - showInfo() { - showModalBottomSheet( - backgroundColor: Colors.black, - barrierColor: Colors.transparent, - isScrollControlled: false, - context: context, - builder: (context) { - return ExifBottomSheet(assetDetail: assetDetail ?? asset); - }, - ); - } - return Stack( children: [ Center( @@ -81,7 +70,7 @@ class ImageViewerPage extends HookConsumerWidget { isZoomedFunction: isZoomedFunction, isZoomedListener: isZoomedListener, onSwipeDown: () => AutoRouter.of(context).pop(), - onSwipeUp: asset.isRemote ? showInfo : () {}, + onSwipeUp: (asset.isRemote && showExifSheet != null) ? showExifSheet! : () {}, ), ), ), diff --git a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart index 5abc30cdb11cc..22d47b71aea3f 100644 --- a/mobile/lib/modules/home/ui/control_bottom_app_bar.dart +++ b/mobile/lib/modules/home/ui/control_bottom_app_bar.dart @@ -5,6 +5,7 @@ import 'package:hive/hive.dart'; import 'package:hooks_riverpod/hooks_riverpod.dart'; import 'package:immich_mobile/constants/hive_box.dart'; import 'package:immich_mobile/modules/home/ui/delete_diaglog.dart'; +import 'package:immich_mobile/shared/ui/drag_sheet.dart'; import 'package:immich_mobile/utils/image_url_builder.dart'; import 'package:openapi/api.dart'; @@ -200,53 +201,3 @@ class AddToAlbumTitleRow extends StatelessWidget { ); } } - -class CustomDraggingHandle extends StatelessWidget { - const CustomDraggingHandle({super.key}); - - @override - Widget build(BuildContext context) { - return Container( - height: 5, - width: 30, - decoration: BoxDecoration( - color: Colors.grey[500], - borderRadius: BorderRadius.circular(16), - ), - ); - } -} - -class ControlBoxButton extends StatelessWidget { - const ControlBoxButton({ - Key? key, - required this.label, - required this.iconData, - required this.onPressed, - }) : super(key: key); - - final String label; - final IconData iconData; - final Function onPressed; - - @override - Widget build(BuildContext context) { - return MaterialButton( - padding: const EdgeInsets.all(10), - shape: const CircleBorder(), - onPressed: () => onPressed(), - child: Column( - mainAxisAlignment: MainAxisAlignment.start, - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Icon(iconData, size: 24), - const SizedBox(height: 6), - Text( - label, - style: const TextStyle(fontSize: 12.0), - ), - ], - ), - ); - } -} diff --git a/mobile/lib/shared/ui/drag_sheet.dart b/mobile/lib/shared/ui/drag_sheet.dart new file mode 100644 index 0000000000000..1b57a1909ebcb --- /dev/null +++ b/mobile/lib/shared/ui/drag_sheet.dart @@ -0,0 +1,51 @@ +import 'package:flutter/material.dart'; + +class CustomDraggingHandle extends StatelessWidget { + const CustomDraggingHandle({super.key}); + + @override + Widget build(BuildContext context) { + return Container( + height: 5, + width: 30, + decoration: BoxDecoration( + color: Colors.grey[500], + borderRadius: BorderRadius.circular(16), + ), + ); + } +} + +class ControlBoxButton extends StatelessWidget { + const ControlBoxButton({ + Key? key, + required this.label, + required this.iconData, + required this.onPressed, + }) : super(key: key); + + final String label; + final IconData iconData; + final Function onPressed; + + @override + Widget build(BuildContext context) { + return MaterialButton( + padding: const EdgeInsets.all(10), + shape: const CircleBorder(), + onPressed: () => onPressed(), + child: Column( + mainAxisAlignment: MainAxisAlignment.start, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Icon(iconData, size: 24), + const SizedBox(height: 6), + Text( + label, + style: const TextStyle(fontSize: 12.0), + ), + ], + ), + ); + } +}