You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat(mobile): Rework of the exif sheet (#1213)
* Draggable sheet in asset viewer page * Minor improvements * Fix display bug * Fix some styling Co-authored-by: Alex <alex.tran1502@gmail.com>
This commit is contained in:
		
							
								
								
									
										
											BIN
										
									
								
								mobile/flutter_01.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								mobile/flutter_01.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 536 KiB | 
| @@ -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(), | ||||
|     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(), | ||||
|               ), | ||||
|               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(), | ||||
|           ), | ||||
|               const SizedBox(height: 12), | ||||
|               if (exifInfo?.dateTimeOriginal != null) | ||||
|                 Text( | ||||
|                   DateFormat('date_format'.tr()).format( | ||||
|                     exifInfo!.dateTimeOriginal!.toLocal(), | ||||
|                   ), | ||||
|                   style: const TextStyle( | ||||
|                     fontWeight: FontWeight.bold, | ||||
|                     fontSize: 14, | ||||
|                   ), | ||||
|                 ), | ||||
|  | ||||
|           // 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]), | ||||
|                   ) | ||||
|                 ], | ||||
|               ), | ||||
|             ), | ||||
|           // 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], | ||||
|                   ), | ||||
|                   Padding( | ||||
|                     padding: const EdgeInsets.only(bottom: 8.0), | ||||
|                     child: Text( | ||||
|                       "exif_bottom_sheet_details", | ||||
|                       style: TextStyle(fontSize: 11, color: Colors.grey[400]), | ||||
|                     ).tr(), | ||||
|                   ), | ||||
|                   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, | ||||
|                   ), | ||||
|                   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), | ||||
|               // Location | ||||
|               if (assetDetail.latitude != null) | ||||
|                 Padding( | ||||
|                   padding: const EdgeInsets.only(top: 32.0), | ||||
|                   child: Column( | ||||
|                     crossAxisAlignment: CrossAxisAlignment.start, | ||||
|                     children: [ | ||||
|                       const Divider( | ||||
|                         thickness: 1, | ||||
|                       ), | ||||
|                       subtitle: Text( | ||||
|                         "ƒ/${exifInfo.fNumber}   1/${(1 / (exifInfo.exposureTime ?? 1)).toStringAsFixed(0)}   ${exifInfo.focalLength} mm   ISO${exifInfo.iso} ", | ||||
|                       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), | ||||
|                       ) | ||||
|                     ], | ||||
|                   ), | ||||
|                 ), | ||||
|               // 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], | ||||
|                       ), | ||||
|                     ), | ||||
|                 ], | ||||
|                       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, | ||||
|               ), | ||||
|             ), | ||||
|         ], | ||||
|             ], | ||||
|           ), | ||||
|         ), | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
|   | ||||
| @@ -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 { | ||||
|   | ||||
| @@ -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<bool> 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! : () {}, | ||||
|             ), | ||||
|           ), | ||||
|         ), | ||||
|   | ||||
| @@ -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), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
|   | ||||
							
								
								
									
										51
									
								
								mobile/lib/shared/ui/drag_sheet.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										51
									
								
								mobile/lib/shared/ui/drag_sheet.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -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), | ||||
|           ), | ||||
|         ], | ||||
|       ), | ||||
|     ); | ||||
|   } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user