You've already forked immich
							
							
				mirror of
				https://github.com/immich-app/immich.git
				synced 2025-10-31 00:18:28 +02:00 
			
		
		
		
	feat: Add description (#2237)
* Added dto, logic to insert description and web implementation * create text field and update on remote database * Update description and save changes * styling * fix web test * fix server test * preserve description on metadata extraction job run * handle exif info is null situation * pr feedback * format openapi spec * update createAssetDto * refactor logic to service * move files * only owner can update description * Render description correctly in shared album * Render description correctly in shared link * disable description edit for not owner of asset on mobile * localization and clean up * fix test * Uses providers for description text (#2244) * uses providers for description text * comments * fixes initial data setting * fixes notifier --------- Co-authored-by: martyfuhry <martyfuhry@gmail.com>
This commit is contained in:
		| @@ -262,5 +262,11 @@ | |||||||
|   "version_announcement_overlay_text_1": "Hi friend, there is a new release of", |   "version_announcement_overlay_text_1": "Hi friend, there is a new release of", | ||||||
|   "version_announcement_overlay_text_2": "please take your time to visit the ", |   "version_announcement_overlay_text_2": "please take your time to visit the ", | ||||||
|   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", |   "version_announcement_overlay_text_3": " and ensure your docker-compose and .env setup is up-to-date to prevent any misconfigurations, especially if you use WatchTower or any mechanism that handles updating your server application automatically.", | ||||||
|   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89" |   "version_announcement_overlay_title": "New Server Version Available \uD83C\uDF89", | ||||||
|  |   "advanced_settings_tile_title": "Advanced", | ||||||
|  |   "advanced_settings_tile_subtitle": "Advanced user's settings", | ||||||
|  |   "advanced_settings_troubleshooting_title": "Troubleshooting", | ||||||
|  |   "advanced_settings_troubleshooting_subtitle": "Enable additional features for troubleshooting", | ||||||
|  |   "description_input_submit_error": "Error updating description, check the log for more details", | ||||||
|  |   "description_input_hint_text": "Add description..." | ||||||
| } | } | ||||||
							
								
								
									
										
											BIN
										
									
								
								mobile/flutter_01.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								mobile/flutter_01.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 679 KiB | 
| @@ -0,0 +1,93 @@ | |||||||
|  | import 'dart:async'; | ||||||
|  |  | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/modules/asset_viewer/services/asset_description.service.dart'; | ||||||
|  | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
|  | import 'package:immich_mobile/shared/models/exif_info.dart'; | ||||||
|  | import 'package:immich_mobile/shared/providers/db.provider.dart'; | ||||||
|  | import 'package:isar/isar.dart'; | ||||||
|  |  | ||||||
|  | class AssetDescriptionNotifier extends StateNotifier<String> { | ||||||
|  |   final Isar _db; | ||||||
|  |   final AssetDescriptionService _service; | ||||||
|  |   final Asset _asset; | ||||||
|  |  | ||||||
|  |   AssetDescriptionNotifier( | ||||||
|  |     this._db, | ||||||
|  |     this._service,  | ||||||
|  |     this._asset, | ||||||
|  |   ) : super('') { | ||||||
|  |     _fetchLocalDescription(); | ||||||
|  |     _fetchRemoteDescription(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   String get description => state; | ||||||
|  |  | ||||||
|  |   /// Fetches the local database value for description | ||||||
|  |   /// and writes it to [state] | ||||||
|  |   void _fetchLocalDescription() async { | ||||||
|  |     final localExifId = _asset.exifInfo?.id; | ||||||
|  |  | ||||||
|  |     // Guard [localExifId] null | ||||||
|  |     if (localExifId == null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Subscribe to local changes | ||||||
|  |     final exifInfo = await _db | ||||||
|  |         .exifInfos | ||||||
|  |         .get(localExifId); | ||||||
|  |  | ||||||
|  |     // Guard | ||||||
|  |     if (exifInfo?.description == null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     state = exifInfo!.description!; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Fetches the remote value and sets the state | ||||||
|  |   void _fetchRemoteDescription() async { | ||||||
|  |     final remoteAssetId = _asset.remoteId; | ||||||
|  |     final localExifId = _asset.exifInfo?.id; | ||||||
|  |  | ||||||
|  |     // Guard [remoteAssetId] and [localExifId] null | ||||||
|  |     if (remoteAssetId == null || localExifId == null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // Reads the latest from the remote and writes it to DB in the service | ||||||
|  |     final latest = await _service.readLatest(remoteAssetId, localExifId); | ||||||
|  |  | ||||||
|  |     state = latest; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   /// Sets the description to [description] | ||||||
|  |   /// Uses the service to set the asset value | ||||||
|  |   Future<void> setDescription(String description) async { | ||||||
|  |     state = description; | ||||||
|  |  | ||||||
|  |     final remoteAssetId = _asset.remoteId; | ||||||
|  |     final localExifId = _asset.exifInfo?.id; | ||||||
|  |  | ||||||
|  |     // Guard [remoteAssetId] and [localExifId] null | ||||||
|  |     if (remoteAssetId == null || localExifId == null) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return _service | ||||||
|  |         .setDescription(description, remoteAssetId, localExifId); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | final assetDescriptionProvider = StateNotifierProvider | ||||||
|  |     .autoDispose | ||||||
|  |     .family<AssetDescriptionNotifier, String, Asset>( | ||||||
|  |   (ref, asset) => AssetDescriptionNotifier( | ||||||
|  |     ref.watch(dbProvider), | ||||||
|  |     ref.watch(assetDescriptionServiceProvider), | ||||||
|  |     asset, | ||||||
|  |   ), | ||||||
|  | ); | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -0,0 +1,62 @@ | |||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/shared/models/exif_info.dart'; | ||||||
|  | import 'package:immich_mobile/shared/providers/api.provider.dart'; | ||||||
|  | import 'package:immich_mobile/shared/providers/db.provider.dart'; | ||||||
|  | import 'package:immich_mobile/shared/services/api.service.dart'; | ||||||
|  | import 'package:isar/isar.dart'; | ||||||
|  | import 'package:openapi/api.dart'; | ||||||
|  |  | ||||||
|  | class AssetDescriptionService { | ||||||
|  |   AssetDescriptionService(this._db, this._api); | ||||||
|  |  | ||||||
|  |   final Isar _db; | ||||||
|  |   final ApiService _api; | ||||||
|  |  | ||||||
|  |   setDescription( | ||||||
|  |     String description, | ||||||
|  |     String remoteAssetId, | ||||||
|  |     int localExifId, | ||||||
|  |   ) async { | ||||||
|  |     final result = await _api.assetApi.updateAsset( | ||||||
|  |       remoteAssetId, | ||||||
|  |       UpdateAssetDto(description: description), | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     if (result?.exifInfo?.description != null) { | ||||||
|  |       var exifInfo = await _db.exifInfos.get(localExifId); | ||||||
|  |  | ||||||
|  |       if (exifInfo != null) { | ||||||
|  |         exifInfo.description = result!.exifInfo!.description; | ||||||
|  |         await _db.writeTxn( | ||||||
|  |           () => _db.exifInfos.put(exifInfo), | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   Future<String> readLatest(String assetRemoteId, int localExifId) async { | ||||||
|  |     final latestAssetFromServer = | ||||||
|  |         await _api.assetApi.getAssetById(assetRemoteId); | ||||||
|  |     final localExifInfo = await _db.exifInfos.get(localExifId); | ||||||
|  |  | ||||||
|  |     if (latestAssetFromServer != null && localExifInfo != null) { | ||||||
|  |       localExifInfo.description = | ||||||
|  |           latestAssetFromServer.exifInfo?.description ?? ''; | ||||||
|  |  | ||||||
|  |       await _db.writeTxn( | ||||||
|  |         () => _db.exifInfos.put(localExifInfo), | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       return localExifInfo.description!; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return ""; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | final assetDescriptionServiceProvider = Provider( | ||||||
|  |   (ref) => AssetDescriptionService( | ||||||
|  |     ref.watch(dbProvider), | ||||||
|  |     ref.watch(apiServiceProvider), | ||||||
|  |   ), | ||||||
|  | ); | ||||||
							
								
								
									
										103
									
								
								mobile/lib/modules/asset_viewer/ui/description_input.dart
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										103
									
								
								mobile/lib/modules/asset_viewer/ui/description_input.dart
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,103 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
|  | import 'package:flutter/material.dart'; | ||||||
|  | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
|  | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/modules/asset_viewer/providers/asset_description.provider.dart'; | ||||||
|  | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
|  | import 'package:immich_mobile/shared/ui/immich_toast.dart'; | ||||||
|  | import 'package:logging/logging.dart'; | ||||||
|  | import 'package:immich_mobile/shared/models/store.dart' as store; | ||||||
|  |  | ||||||
|  | class DescriptionInput extends HookConsumerWidget { | ||||||
|  |   DescriptionInput({ | ||||||
|  |     super.key, | ||||||
|  |     required this.asset, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   final Asset asset; | ||||||
|  |   final Logger _log = Logger('DescriptionInput'); | ||||||
|  |  | ||||||
|  |   @override | ||||||
|  |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|  |     final isDarkTheme = Theme.of(context).brightness == Brightness.dark; | ||||||
|  |     final textColor = isDarkTheme ? Colors.white : Colors.black; | ||||||
|  |     final controller = useTextEditingController(); | ||||||
|  |     final focusNode = useFocusNode(); | ||||||
|  |     final isFocus = useState(false); | ||||||
|  |     final isTextEmpty = useState(controller.text.isEmpty); | ||||||
|  |     final descriptionProvider = ref.watch(assetDescriptionProvider(asset).notifier); | ||||||
|  |     final description = ref.watch(assetDescriptionProvider(asset)); | ||||||
|  |     final owner = store.Store.get(store.StoreKey.currentUser); | ||||||
|  |     final hasError = useState(false); | ||||||
|  |  | ||||||
|  |     controller.text = description; | ||||||
|  |  | ||||||
|  |     submitDescription(String description) async { | ||||||
|  |       hasError.value = false; | ||||||
|  |       try { | ||||||
|  |         await descriptionProvider.setDescription( | ||||||
|  |           description, | ||||||
|  |         ); | ||||||
|  |       } catch (error, stack) { | ||||||
|  |         hasError.value = true; | ||||||
|  |         _log.severe("Error updating description $error", error, stack); | ||||||
|  |         ImmichToast.show( | ||||||
|  |           context: context, | ||||||
|  |           msg: "description_input_submit_error".tr(), | ||||||
|  |           toastType: ToastType.error, | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     Widget? suffixIcon; | ||||||
|  |     if (hasError.value) { | ||||||
|  |       suffixIcon = const Icon(Icons.warning_outlined); | ||||||
|  |     } else if (!isTextEmpty.value && isFocus.value) { | ||||||
|  |       suffixIcon = IconButton( | ||||||
|  |         onPressed: () { | ||||||
|  |           controller.clear(); | ||||||
|  |           isTextEmpty.value = true; | ||||||
|  |         }, | ||||||
|  |         icon: Icon( | ||||||
|  |           Icons.cancel_rounded, | ||||||
|  |           color: Colors.grey[500], | ||||||
|  |         ), | ||||||
|  |         splashRadius: 10, | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return TextField( | ||||||
|  |       enabled: owner.isarId == asset.ownerId, | ||||||
|  |       focusNode: focusNode, | ||||||
|  |       onTap: () => isFocus.value = true, | ||||||
|  |       onChanged: (value) { | ||||||
|  |         isTextEmpty.value = false; | ||||||
|  |       }, | ||||||
|  |       onTapOutside: (a) async { | ||||||
|  |         isFocus.value = false; | ||||||
|  |         focusNode.unfocus(); | ||||||
|  |  | ||||||
|  |         if (description != controller.text) { | ||||||
|  |           await submitDescription(controller.text); | ||||||
|  |         } | ||||||
|  |       }, | ||||||
|  |       autofocus: false, | ||||||
|  |       maxLines: null, | ||||||
|  |       keyboardType: TextInputType.multiline, | ||||||
|  |       controller: controller, | ||||||
|  |       style: const TextStyle( | ||||||
|  |         fontSize: 14, | ||||||
|  |       ), | ||||||
|  |       decoration: InputDecoration( | ||||||
|  |         hintText: 'description_input_hint_text'.tr(), | ||||||
|  |         border: InputBorder.none, | ||||||
|  |         hintStyle: TextStyle( | ||||||
|  |           fontWeight: FontWeight.normal, | ||||||
|  |           fontSize: 12, | ||||||
|  |           color: textColor.withOpacity(0.5), | ||||||
|  |         ), | ||||||
|  |         suffixIcon: suffixIcon, | ||||||
|  |       ), | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -2,25 +2,25 @@ import 'package:easy_localization/easy_localization.dart'; | |||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_map/flutter_map.dart'; | import 'package:flutter_map/flutter_map.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
|  | import 'package:immich_mobile/modules/asset_viewer/ui/description_input.dart'; | ||||||
| import 'package:immich_mobile/shared/models/asset.dart'; | import 'package:immich_mobile/shared/models/asset.dart'; | ||||||
| import 'package:immich_mobile/shared/models/exif_info.dart'; |  | ||||||
| import 'package:immich_mobile/shared/ui/drag_sheet.dart'; | import 'package:immich_mobile/shared/ui/drag_sheet.dart'; | ||||||
| import 'package:latlong2/latlong.dart'; | import 'package:latlong2/latlong.dart'; | ||||||
| import 'package:immich_mobile/utils/bytes_units.dart'; | import 'package:immich_mobile/utils/bytes_units.dart'; | ||||||
|  |  | ||||||
| class ExifBottomSheet extends HookConsumerWidget { | class ExifBottomSheet extends HookConsumerWidget { | ||||||
|   final Asset assetDetail; |   final Asset asset; | ||||||
|  |  | ||||||
|   const ExifBottomSheet({Key? key, required this.assetDetail}) |   const ExifBottomSheet({Key? key, required this.asset}) : super(key: key); | ||||||
|       : super(key: key); |  | ||||||
|  |  | ||||||
|   bool get showMap => |   bool get showMap => | ||||||
|       assetDetail.exifInfo?.latitude != null && |       asset.exifInfo?.latitude != null && asset.exifInfo?.longitude != null; | ||||||
|       assetDetail.exifInfo?.longitude != null; |  | ||||||
|  |  | ||||||
|   @override |   @override | ||||||
|   Widget build(BuildContext context, WidgetRef ref) { |   Widget build(BuildContext context, WidgetRef ref) { | ||||||
|     final ExifInfo? exifInfo = assetDetail.exifInfo; |     final exifInfo = asset.exifInfo; | ||||||
|  |     var isDarkTheme = Theme.of(context).brightness == Brightness.dark; | ||||||
|  |     var textColor = isDarkTheme ? Colors.white : Colors.black; | ||||||
|  |  | ||||||
|     buildMap() { |     buildMap() { | ||||||
|       return Padding( |       return Padding( | ||||||
| @@ -76,19 +76,6 @@ class ExifBottomSheet extends HookConsumerWidget { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     final textColor = Theme.of(context).primaryColor; |  | ||||||
|  |  | ||||||
|     buildLocationText() { |  | ||||||
|       return Text( |  | ||||||
|         "${exifInfo?.city}, ${exifInfo?.state}", |  | ||||||
|         style: TextStyle( |  | ||||||
|           fontSize: 12, |  | ||||||
|           fontWeight: FontWeight.bold, |  | ||||||
|           color: textColor, |  | ||||||
|         ), |  | ||||||
|       ); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     buildSizeText(Asset a) { |     buildSizeText(Asset a) { | ||||||
|       String resolution = a.width != null && a.height != null |       String resolution = a.width != null && a.height != null | ||||||
|           ? "${a.height} x ${a.width}  " |           ? "${a.height} x ${a.width}  " | ||||||
| @@ -128,13 +115,39 @@ class ExifBottomSheet extends HookConsumerWidget { | |||||||
|             children: [ |             children: [ | ||||||
|               Text( |               Text( | ||||||
|                 "exif_bottom_sheet_location", |                 "exif_bottom_sheet_location", | ||||||
|                 style: TextStyle(fontSize: 11, color: textColor), |                 style: TextStyle( | ||||||
|  |                   fontSize: 11, | ||||||
|  |                   color: textColor, | ||||||
|  |                   fontWeight: FontWeight.bold, | ||||||
|  |                 ), | ||||||
|               ).tr(), |               ).tr(), | ||||||
|               buildMap(), |               buildMap(), | ||||||
|               if (exifInfo != null && |               RichText( | ||||||
|                   exifInfo.city != null && |                 text: TextSpan( | ||||||
|                   exifInfo.state != null) |                   style: TextStyle( | ||||||
|                 buildLocationText(), |                     fontSize: 12, | ||||||
|  |                     fontWeight: FontWeight.bold, | ||||||
|  |                     color: textColor, | ||||||
|  |                     fontFamily: 'WorkSans', | ||||||
|  |                   ), | ||||||
|  |                   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( |               Text( | ||||||
|                 "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", |                 "${exifInfo!.latitude!.toStringAsFixed(4)}, ${exifInfo.longitude!.toStringAsFixed(4)}", | ||||||
|                 style: const TextStyle(fontSize: 12), |                 style: const TextStyle(fontSize: 12), | ||||||
| @@ -146,7 +159,7 @@ class ExifBottomSheet extends HookConsumerWidget { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     buildDate() { |     buildDate() { | ||||||
|       final fileCreatedAt = assetDetail.fileCreatedAt.toLocal(); |       final fileCreatedAt = asset.fileCreatedAt.toLocal(); | ||||||
|       final date = DateFormat.yMMMEd().format(fileCreatedAt); |       final date = DateFormat.yMMMEd().format(fileCreatedAt); | ||||||
|       final time = DateFormat.jm().format(fileCreatedAt); |       final time = DateFormat.jm().format(fileCreatedAt); | ||||||
|  |  | ||||||
| @@ -167,27 +180,37 @@ class ExifBottomSheet extends HookConsumerWidget { | |||||||
|             padding: const EdgeInsets.only(bottom: 8.0), |             padding: const EdgeInsets.only(bottom: 8.0), | ||||||
|             child: Text( |             child: Text( | ||||||
|               "exif_bottom_sheet_details", |               "exif_bottom_sheet_details", | ||||||
|               style: TextStyle(fontSize: 11, color: textColor), |               style: TextStyle( | ||||||
|  |                 fontSize: 11, | ||||||
|  |                 color: textColor, | ||||||
|  |                 fontWeight: FontWeight.bold, | ||||||
|  |               ), | ||||||
|             ).tr(), |             ).tr(), | ||||||
|           ), |           ), | ||||||
|           ListTile( |           ListTile( | ||||||
|             contentPadding: const EdgeInsets.all(0), |             contentPadding: const EdgeInsets.all(0), | ||||||
|             dense: true, |             dense: true, | ||||||
|             leading: const Icon(Icons.image), |             leading: Icon( | ||||||
|  |               Icons.image, | ||||||
|  |               color: textColor.withAlpha(200), | ||||||
|  |             ), | ||||||
|             title: Text( |             title: Text( | ||||||
|               assetDetail.fileName, |               asset.fileName, | ||||||
|               style: TextStyle( |               style: TextStyle( | ||||||
|                 fontWeight: FontWeight.bold, |                 fontWeight: FontWeight.bold, | ||||||
|                 color: textColor, |                 color: textColor, | ||||||
|               ), |               ), | ||||||
|             ), |             ), | ||||||
|             subtitle: buildSizeText(assetDetail), |             subtitle: buildSizeText(asset), | ||||||
|           ), |           ), | ||||||
|           if (exifInfo?.make != null) |           if (exifInfo?.make != null) | ||||||
|             ListTile( |             ListTile( | ||||||
|               contentPadding: const EdgeInsets.all(0), |               contentPadding: const EdgeInsets.all(0), | ||||||
|               dense: true, |               dense: true, | ||||||
|               leading: const Icon(Icons.camera), |               leading: Icon( | ||||||
|  |                 Icons.camera, | ||||||
|  |                 color: textColor.withAlpha(200), | ||||||
|  |               ), | ||||||
|               title: Text( |               title: Text( | ||||||
|                 "${exifInfo!.make} ${exifInfo.model}", |                 "${exifInfo!.make} ${exifInfo.model}", | ||||||
|                 style: TextStyle( |                 style: TextStyle( | ||||||
| @@ -203,80 +226,75 @@ class ExifBottomSheet extends HookConsumerWidget { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return SingleChildScrollView( |     return GestureDetector( | ||||||
|       child: Card( |       onTap: () { | ||||||
|         shape: const RoundedRectangleBorder( |         // FocusScope.of(context).unfocus(); | ||||||
|           borderRadius: BorderRadius.only( |       }, | ||||||
|             topLeft: Radius.circular(15), |       child: SingleChildScrollView( | ||||||
|             topRight: Radius.circular(15), |         child: Card( | ||||||
|  |           shape: const RoundedRectangleBorder( | ||||||
|  |             borderRadius: BorderRadius.only( | ||||||
|  |               topLeft: Radius.circular(15), | ||||||
|  |               topRight: Radius.circular(15), | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|         ), |           margin: const EdgeInsets.all(0), | ||||||
|         margin: const EdgeInsets.all(0), |           child: Container( | ||||||
|         child: Container( |             margin: const EdgeInsets.symmetric(horizontal: 16.0), | ||||||
|           margin: const EdgeInsets.symmetric(horizontal: 8.0), |             child: LayoutBuilder( | ||||||
|           child: LayoutBuilder( |               builder: (context, constraints) { | ||||||
|             builder: (context, constraints) { |                 if (constraints.maxWidth > 600) { | ||||||
|               if (constraints.maxWidth > 600) { |                   // Two column | ||||||
|                 // Two column |                   return Padding( | ||||||
|                 return Padding( |                     padding: const EdgeInsets.symmetric(horizontal: 12.0), | ||||||
|                   padding: const EdgeInsets.symmetric(horizontal: 12.0), |                     child: Column( | ||||||
|                   child: Column( |                       crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                     crossAxisAlignment: CrossAxisAlignment.stretch, |                       children: [ | ||||||
|                     children: [ |                         buildDragHeader(), | ||||||
|                       buildDragHeader(), |                         buildDate(), | ||||||
|                       buildDate(), |                         if (asset.isRemote) DescriptionInput(asset: asset), | ||||||
|                       const SizedBox(height: 32.0), |                         Row( | ||||||
|                       Row( |                           mainAxisAlignment: MainAxisAlignment.spaceEvenly, | ||||||
|                         mainAxisAlignment: MainAxisAlignment.spaceEvenly, |                           crossAxisAlignment: CrossAxisAlignment.start, | ||||||
|                         crossAxisAlignment: CrossAxisAlignment.start, |                           children: [ | ||||||
|                         children: [ |                             Flexible( | ||||||
|                           Flexible( |                               flex: showMap ? 5 : 0, | ||||||
|                             flex: showMap ? 5 : 0, |                               child: Padding( | ||||||
|                             child: Padding( |                                 padding: const EdgeInsets.only(right: 8.0), | ||||||
|                               padding: const EdgeInsets.only(right: 8.0), |                                 child: buildLocation(), | ||||||
|                               child: buildLocation(), |                               ), | ||||||
|                             ), |                             ), | ||||||
|                           ), |                             Flexible( | ||||||
|                           Flexible( |                               flex: 5, | ||||||
|                             flex: 5, |                               child: Padding( | ||||||
|                             child: Padding( |                                 padding: const EdgeInsets.only(left: 8.0), | ||||||
|                               padding: const EdgeInsets.only(left: 8.0), |                                 child: buildDetail(), | ||||||
|                               child: buildDetail(), |                               ), | ||||||
|                             ), |                             ), | ||||||
|                           ), |                           ], | ||||||
|                         ], |                         ), | ||||||
|                       ), |                         const SizedBox(height: 50), | ||||||
|                       const SizedBox(height: 50), |                       ], | ||||||
|                     ], |  | ||||||
|                   ), |  | ||||||
|                 ); |  | ||||||
|               } |  | ||||||
|  |  | ||||||
|               // One column |  | ||||||
|               return Column( |  | ||||||
|                 crossAxisAlignment: CrossAxisAlignment.stretch, |  | ||||||
|                 children: [ |  | ||||||
|                   buildDragHeader(), |  | ||||||
|                   buildDate(), |  | ||||||
|                   const SizedBox(height: 16.0), |  | ||||||
|                   if (showMap) |  | ||||||
|                     Divider( |  | ||||||
|                       thickness: 1, |  | ||||||
|                       color: Colors.grey[600], |  | ||||||
|                     ), |                     ), | ||||||
|                   const SizedBox(height: 16.0), |                   ); | ||||||
|                   buildLocation(), |                 } | ||||||
|                   const SizedBox(height: 16.0), |  | ||||||
|                   Divider( |                 // One column | ||||||
|                     thickness: 1, |                 return Column( | ||||||
|                     color: Colors.grey[600], |                   crossAxisAlignment: CrossAxisAlignment.stretch, | ||||||
|                   ), |                   children: [ | ||||||
|                   const SizedBox(height: 16.0), |                     buildDragHeader(), | ||||||
|                   buildDetail(), |                     buildDate(), | ||||||
|                   const SizedBox(height: 50), |                     if (asset.isRemote) DescriptionInput(asset: asset), | ||||||
|                 ], |                     const SizedBox(height: 8.0), | ||||||
|               ); |                     buildLocation(), | ||||||
|             }, |                     SizedBox(height: showMap ? 16.0 : 0.0), | ||||||
|  |                     buildDetail(), | ||||||
|  |                     const SizedBox(height: 50), | ||||||
|  |                   ], | ||||||
|  |                 ); | ||||||
|  |               }, | ||||||
|  |             ), | ||||||
|           ), |           ), | ||||||
|         ), |         ), | ||||||
|       ), |       ), | ||||||
|   | |||||||
| @@ -195,7 +195,12 @@ class GalleryViewerPage extends HookConsumerWidget { | |||||||
|               .getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) { |               .getSetting<bool>(AppSettingsEnum.advancedTroubleshooting)) { | ||||||
|             return AdvancedBottomSheet(assetDetail: assetDetail!); |             return AdvancedBottomSheet(assetDetail: assetDetail!); | ||||||
|           } |           } | ||||||
|           return ExifBottomSheet(assetDetail: assetDetail!); |           return Padding( | ||||||
|  |             padding: EdgeInsets.only( | ||||||
|  |               bottom: MediaQuery.of(context).viewInsets.bottom, | ||||||
|  |             ), | ||||||
|  |             child: ExifBottomSheet(asset: assetDetail!), | ||||||
|  |           ); | ||||||
|         }, |         }, | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import 'package:easy_localization/easy_localization.dart'; | ||||||
| import 'package:flutter/material.dart'; | import 'package:flutter/material.dart'; | ||||||
| import 'package:flutter_hooks/flutter_hooks.dart'; | import 'package:flutter_hooks/flutter_hooks.dart'; | ||||||
| import 'package:hooks_riverpod/hooks_riverpod.dart'; | import 'package:hooks_riverpod/hooks_riverpod.dart'; | ||||||
| @@ -25,19 +26,25 @@ class AdvancedSettings extends HookConsumerWidget { | |||||||
|     return ExpansionTile( |     return ExpansionTile( | ||||||
|       textColor: Theme.of(context).primaryColor, |       textColor: Theme.of(context).primaryColor, | ||||||
|       title: const Text( |       title: const Text( | ||||||
|         "Advanced", |         "advanced_settings_tile_title", | ||||||
|         style: TextStyle( |         style: TextStyle( | ||||||
|           fontWeight: FontWeight.bold, |           fontWeight: FontWeight.bold, | ||||||
|         ), |         ), | ||||||
|       ), |       ).tr(), | ||||||
|  |       subtitle: const Text( | ||||||
|  |         "advanced_settings_tile_subtitle", | ||||||
|  |         style: TextStyle( | ||||||
|  |           fontSize: 13, | ||||||
|  |         ), | ||||||
|  |       ).tr(), | ||||||
|       children: [ |       children: [ | ||||||
|         SettingsSwitchListTile( |         SettingsSwitchListTile( | ||||||
|           enabled: true, |           enabled: true, | ||||||
|           appSettingService: appSettingService, |           appSettingService: appSettingService, | ||||||
|           valueNotifier: isEnabled, |           valueNotifier: isEnabled, | ||||||
|           settingsEnum: AppSettingsEnum.advancedTroubleshooting, |           settingsEnum: AppSettingsEnum.advancedTroubleshooting, | ||||||
|           title: "Troubleshooting", |           title: "advanced_settings_troubleshooting_title".tr(), | ||||||
|           subtitle: "Enable additional features for troubleshooting", |           subtitle: "advanced_settings_troubleshooting_subtitle".tr(), | ||||||
|         ), |         ), | ||||||
|       ], |       ], | ||||||
|     ); |     ); | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ class ExifInfo { | |||||||
|   String? city; |   String? city; | ||||||
|   String? state; |   String? state; | ||||||
|   String? country; |   String? country; | ||||||
|  |   String? description; | ||||||
|  |  | ||||||
|   @ignore |   @ignore | ||||||
|   String get exposureTime { |   String get exposureTime { | ||||||
| @@ -58,7 +59,8 @@ class ExifInfo { | |||||||
|         long = dto.longitude?.toDouble(), |         long = dto.longitude?.toDouble(), | ||||||
|         city = dto.city, |         city = dto.city, | ||||||
|         state = dto.state, |         state = dto.state, | ||||||
|         country = dto.country; |         country = dto.country, | ||||||
|  |         description = dto.description; | ||||||
|  |  | ||||||
|   ExifInfo({ |   ExifInfo({ | ||||||
|     this.fileSize, |     this.fileSize, | ||||||
| @@ -74,6 +76,7 @@ class ExifInfo { | |||||||
|     this.city, |     this.city, | ||||||
|     this.state, |     this.state, | ||||||
|     this.country, |     this.country, | ||||||
|  |     this.description, | ||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -27,58 +27,63 @@ const ExifInfoSchema = CollectionSchema( | |||||||
|       name: r'country', |       name: r'country', | ||||||
|       type: IsarType.string, |       type: IsarType.string, | ||||||
|     ), |     ), | ||||||
|     r'exposureSeconds': PropertySchema( |     r'description': PropertySchema( | ||||||
|       id: 2, |       id: 2, | ||||||
|  |       name: r'description', | ||||||
|  |       type: IsarType.string, | ||||||
|  |     ), | ||||||
|  |     r'exposureSeconds': PropertySchema( | ||||||
|  |       id: 3, | ||||||
|       name: r'exposureSeconds', |       name: r'exposureSeconds', | ||||||
|       type: IsarType.float, |       type: IsarType.float, | ||||||
|     ), |     ), | ||||||
|     r'f': PropertySchema( |     r'f': PropertySchema( | ||||||
|       id: 3, |       id: 4, | ||||||
|       name: r'f', |       name: r'f', | ||||||
|       type: IsarType.float, |       type: IsarType.float, | ||||||
|     ), |     ), | ||||||
|     r'fileSize': PropertySchema( |     r'fileSize': PropertySchema( | ||||||
|       id: 4, |       id: 5, | ||||||
|       name: r'fileSize', |       name: r'fileSize', | ||||||
|       type: IsarType.long, |       type: IsarType.long, | ||||||
|     ), |     ), | ||||||
|     r'iso': PropertySchema( |     r'iso': PropertySchema( | ||||||
|       id: 5, |       id: 6, | ||||||
|       name: r'iso', |       name: r'iso', | ||||||
|       type: IsarType.int, |       type: IsarType.int, | ||||||
|     ), |     ), | ||||||
|     r'lat': PropertySchema( |     r'lat': PropertySchema( | ||||||
|       id: 6, |       id: 7, | ||||||
|       name: r'lat', |       name: r'lat', | ||||||
|       type: IsarType.float, |       type: IsarType.float, | ||||||
|     ), |     ), | ||||||
|     r'lens': PropertySchema( |     r'lens': PropertySchema( | ||||||
|       id: 7, |       id: 8, | ||||||
|       name: r'lens', |       name: r'lens', | ||||||
|       type: IsarType.string, |       type: IsarType.string, | ||||||
|     ), |     ), | ||||||
|     r'long': PropertySchema( |     r'long': PropertySchema( | ||||||
|       id: 8, |       id: 9, | ||||||
|       name: r'long', |       name: r'long', | ||||||
|       type: IsarType.float, |       type: IsarType.float, | ||||||
|     ), |     ), | ||||||
|     r'make': PropertySchema( |     r'make': PropertySchema( | ||||||
|       id: 9, |       id: 10, | ||||||
|       name: r'make', |       name: r'make', | ||||||
|       type: IsarType.string, |       type: IsarType.string, | ||||||
|     ), |     ), | ||||||
|     r'mm': PropertySchema( |     r'mm': PropertySchema( | ||||||
|       id: 10, |       id: 11, | ||||||
|       name: r'mm', |       name: r'mm', | ||||||
|       type: IsarType.float, |       type: IsarType.float, | ||||||
|     ), |     ), | ||||||
|     r'model': PropertySchema( |     r'model': PropertySchema( | ||||||
|       id: 11, |       id: 12, | ||||||
|       name: r'model', |       name: r'model', | ||||||
|       type: IsarType.string, |       type: IsarType.string, | ||||||
|     ), |     ), | ||||||
|     r'state': PropertySchema( |     r'state': PropertySchema( | ||||||
|       id: 12, |       id: 13, | ||||||
|       name: r'state', |       name: r'state', | ||||||
|       type: IsarType.string, |       type: IsarType.string, | ||||||
|     ) |     ) | ||||||
| @@ -115,6 +120,12 @@ int _exifInfoEstimateSize( | |||||||
|       bytesCount += 3 + value.length * 3; |       bytesCount += 3 + value.length * 3; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |   { | ||||||
|  |     final value = object.description; | ||||||
|  |     if (value != null) { | ||||||
|  |       bytesCount += 3 + value.length * 3; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|   { |   { | ||||||
|     final value = object.lens; |     final value = object.lens; | ||||||
|     if (value != null) { |     if (value != null) { | ||||||
| @@ -150,17 +161,18 @@ void _exifInfoSerialize( | |||||||
| ) { | ) { | ||||||
|   writer.writeString(offsets[0], object.city); |   writer.writeString(offsets[0], object.city); | ||||||
|   writer.writeString(offsets[1], object.country); |   writer.writeString(offsets[1], object.country); | ||||||
|   writer.writeFloat(offsets[2], object.exposureSeconds); |   writer.writeString(offsets[2], object.description); | ||||||
|   writer.writeFloat(offsets[3], object.f); |   writer.writeFloat(offsets[3], object.exposureSeconds); | ||||||
|   writer.writeLong(offsets[4], object.fileSize); |   writer.writeFloat(offsets[4], object.f); | ||||||
|   writer.writeInt(offsets[5], object.iso); |   writer.writeLong(offsets[5], object.fileSize); | ||||||
|   writer.writeFloat(offsets[6], object.lat); |   writer.writeInt(offsets[6], object.iso); | ||||||
|   writer.writeString(offsets[7], object.lens); |   writer.writeFloat(offsets[7], object.lat); | ||||||
|   writer.writeFloat(offsets[8], object.long); |   writer.writeString(offsets[8], object.lens); | ||||||
|   writer.writeString(offsets[9], object.make); |   writer.writeFloat(offsets[9], object.long); | ||||||
|   writer.writeFloat(offsets[10], object.mm); |   writer.writeString(offsets[10], object.make); | ||||||
|   writer.writeString(offsets[11], object.model); |   writer.writeFloat(offsets[11], object.mm); | ||||||
|   writer.writeString(offsets[12], object.state); |   writer.writeString(offsets[12], object.model); | ||||||
|  |   writer.writeString(offsets[13], object.state); | ||||||
| } | } | ||||||
|  |  | ||||||
| ExifInfo _exifInfoDeserialize( | ExifInfo _exifInfoDeserialize( | ||||||
| @@ -172,17 +184,18 @@ ExifInfo _exifInfoDeserialize( | |||||||
|   final object = ExifInfo( |   final object = ExifInfo( | ||||||
|     city: reader.readStringOrNull(offsets[0]), |     city: reader.readStringOrNull(offsets[0]), | ||||||
|     country: reader.readStringOrNull(offsets[1]), |     country: reader.readStringOrNull(offsets[1]), | ||||||
|     exposureSeconds: reader.readFloatOrNull(offsets[2]), |     description: reader.readStringOrNull(offsets[2]), | ||||||
|     f: reader.readFloatOrNull(offsets[3]), |     exposureSeconds: reader.readFloatOrNull(offsets[3]), | ||||||
|     fileSize: reader.readLongOrNull(offsets[4]), |     f: reader.readFloatOrNull(offsets[4]), | ||||||
|     iso: reader.readIntOrNull(offsets[5]), |     fileSize: reader.readLongOrNull(offsets[5]), | ||||||
|     lat: reader.readFloatOrNull(offsets[6]), |     iso: reader.readIntOrNull(offsets[6]), | ||||||
|     lens: reader.readStringOrNull(offsets[7]), |     lat: reader.readFloatOrNull(offsets[7]), | ||||||
|     long: reader.readFloatOrNull(offsets[8]), |     lens: reader.readStringOrNull(offsets[8]), | ||||||
|     make: reader.readStringOrNull(offsets[9]), |     long: reader.readFloatOrNull(offsets[9]), | ||||||
|     mm: reader.readFloatOrNull(offsets[10]), |     make: reader.readStringOrNull(offsets[10]), | ||||||
|     model: reader.readStringOrNull(offsets[11]), |     mm: reader.readFloatOrNull(offsets[11]), | ||||||
|     state: reader.readStringOrNull(offsets[12]), |     model: reader.readStringOrNull(offsets[12]), | ||||||
|  |     state: reader.readStringOrNull(offsets[13]), | ||||||
|   ); |   ); | ||||||
|   object.id = id; |   object.id = id; | ||||||
|   return object; |   return object; | ||||||
| @@ -200,27 +213,29 @@ P _exifInfoDeserializeProp<P>( | |||||||
|     case 1: |     case 1: | ||||||
|       return (reader.readStringOrNull(offset)) as P; |       return (reader.readStringOrNull(offset)) as P; | ||||||
|     case 2: |     case 2: | ||||||
|       return (reader.readFloatOrNull(offset)) as P; |       return (reader.readStringOrNull(offset)) as P; | ||||||
|     case 3: |     case 3: | ||||||
|       return (reader.readFloatOrNull(offset)) as P; |       return (reader.readFloatOrNull(offset)) as P; | ||||||
|     case 4: |     case 4: | ||||||
|       return (reader.readLongOrNull(offset)) as P; |       return (reader.readFloatOrNull(offset)) as P; | ||||||
|     case 5: |     case 5: | ||||||
|       return (reader.readIntOrNull(offset)) as P; |       return (reader.readLongOrNull(offset)) as P; | ||||||
|     case 6: |     case 6: | ||||||
|       return (reader.readFloatOrNull(offset)) as P; |       return (reader.readIntOrNull(offset)) as P; | ||||||
|     case 7: |     case 7: | ||||||
|       return (reader.readStringOrNull(offset)) as P; |       return (reader.readFloatOrNull(offset)) as P; | ||||||
|     case 8: |     case 8: | ||||||
|       return (reader.readFloatOrNull(offset)) as P; |       return (reader.readStringOrNull(offset)) as P; | ||||||
|     case 9: |     case 9: | ||||||
|       return (reader.readStringOrNull(offset)) as P; |  | ||||||
|     case 10: |  | ||||||
|       return (reader.readFloatOrNull(offset)) as P; |       return (reader.readFloatOrNull(offset)) as P; | ||||||
|     case 11: |     case 10: | ||||||
|       return (reader.readStringOrNull(offset)) as P; |       return (reader.readStringOrNull(offset)) as P; | ||||||
|  |     case 11: | ||||||
|  |       return (reader.readFloatOrNull(offset)) as P; | ||||||
|     case 12: |     case 12: | ||||||
|       return (reader.readStringOrNull(offset)) as P; |       return (reader.readStringOrNull(offset)) as P; | ||||||
|  |     case 13: | ||||||
|  |       return (reader.readStringOrNull(offset)) as P; | ||||||
|     default: |     default: | ||||||
|       throw IsarError('Unknown property with id $propertyId'); |       throw IsarError('Unknown property with id $propertyId'); | ||||||
|   } |   } | ||||||
| @@ -607,6 +622,155 @@ extension ExifInfoQueryFilter | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionIsNull() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(const FilterCondition.isNull( | ||||||
|  |         property: r'description', | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> | ||||||
|  |       descriptionIsNotNull() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(const FilterCondition.isNotNull( | ||||||
|  |         property: r'description', | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionEqualTo( | ||||||
|  |     String? value, { | ||||||
|  |     bool caseSensitive = true, | ||||||
|  |   }) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.equalTo( | ||||||
|  |         property: r'description', | ||||||
|  |         value: value, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> | ||||||
|  |       descriptionGreaterThan( | ||||||
|  |     String? value, { | ||||||
|  |     bool include = false, | ||||||
|  |     bool caseSensitive = true, | ||||||
|  |   }) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.greaterThan( | ||||||
|  |         include: include, | ||||||
|  |         property: r'description', | ||||||
|  |         value: value, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionLessThan( | ||||||
|  |     String? value, { | ||||||
|  |     bool include = false, | ||||||
|  |     bool caseSensitive = true, | ||||||
|  |   }) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.lessThan( | ||||||
|  |         include: include, | ||||||
|  |         property: r'description', | ||||||
|  |         value: value, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionBetween( | ||||||
|  |     String? lower, | ||||||
|  |     String? upper, { | ||||||
|  |     bool includeLower = true, | ||||||
|  |     bool includeUpper = true, | ||||||
|  |     bool caseSensitive = true, | ||||||
|  |   }) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.between( | ||||||
|  |         property: r'description', | ||||||
|  |         lower: lower, | ||||||
|  |         includeLower: includeLower, | ||||||
|  |         upper: upper, | ||||||
|  |         includeUpper: includeUpper, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionStartsWith( | ||||||
|  |     String value, { | ||||||
|  |     bool caseSensitive = true, | ||||||
|  |   }) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.startsWith( | ||||||
|  |         property: r'description', | ||||||
|  |         value: value, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionEndsWith( | ||||||
|  |     String value, { | ||||||
|  |     bool caseSensitive = true, | ||||||
|  |   }) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.endsWith( | ||||||
|  |         property: r'description', | ||||||
|  |         value: value, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionContains( | ||||||
|  |       String value, | ||||||
|  |       {bool caseSensitive = true}) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.contains( | ||||||
|  |         property: r'description', | ||||||
|  |         value: value, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionMatches( | ||||||
|  |       String pattern, | ||||||
|  |       {bool caseSensitive = true}) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.matches( | ||||||
|  |         property: r'description', | ||||||
|  |         wildcard: pattern, | ||||||
|  |         caseSensitive: caseSensitive, | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> descriptionIsEmpty() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.equalTo( | ||||||
|  |         property: r'description', | ||||||
|  |         value: '', | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> | ||||||
|  |       descriptionIsNotEmpty() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addFilterCondition(FilterCondition.greaterThan( | ||||||
|  |         property: r'description', | ||||||
|  |         value: '', | ||||||
|  |       )); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> |   QueryBuilder<ExifInfo, ExifInfo, QAfterFilterCondition> | ||||||
|       exposureSecondsIsNull() { |       exposureSecondsIsNull() { | ||||||
|     return QueryBuilder.apply(this, (query) { |     return QueryBuilder.apply(this, (query) { | ||||||
| @@ -1825,6 +1989,18 @@ extension ExifInfoQuerySortBy on QueryBuilder<ExifInfo, ExifInfo, QSortBy> { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByDescription() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addSortBy(r'description', Sort.asc); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByDescriptionDesc() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addSortBy(r'description', Sort.desc); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByExposureSeconds() { |   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> sortByExposureSeconds() { | ||||||
|     return QueryBuilder.apply(this, (query) { |     return QueryBuilder.apply(this, (query) { | ||||||
|       return query.addSortBy(r'exposureSeconds', Sort.asc); |       return query.addSortBy(r'exposureSeconds', Sort.asc); | ||||||
| @@ -1984,6 +2160,18 @@ extension ExifInfoQuerySortThenBy | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByDescription() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addSortBy(r'description', Sort.asc); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByDescriptionDesc() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addSortBy(r'description', Sort.desc); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByExposureSeconds() { |   QueryBuilder<ExifInfo, ExifInfo, QAfterSortBy> thenByExposureSeconds() { | ||||||
|     return QueryBuilder.apply(this, (query) { |     return QueryBuilder.apply(this, (query) { | ||||||
|       return query.addSortBy(r'exposureSeconds', Sort.asc); |       return query.addSortBy(r'exposureSeconds', Sort.asc); | ||||||
| @@ -2145,6 +2333,13 @@ extension ExifInfoQueryWhereDistinct | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByDescription( | ||||||
|  |       {bool caseSensitive = true}) { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addDistinctBy(r'description', caseSensitive: caseSensitive); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByExposureSeconds() { |   QueryBuilder<ExifInfo, ExifInfo, QDistinct> distinctByExposureSeconds() { | ||||||
|     return QueryBuilder.apply(this, (query) { |     return QueryBuilder.apply(this, (query) { | ||||||
|       return query.addDistinctBy(r'exposureSeconds'); |       return query.addDistinctBy(r'exposureSeconds'); | ||||||
| @@ -2236,6 +2431,12 @@ extension ExifInfoQueryProperty | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   QueryBuilder<ExifInfo, String?, QQueryOperations> descriptionProperty() { | ||||||
|  |     return QueryBuilder.apply(this, (query) { | ||||||
|  |       return query.addPropertyName(r'description'); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   QueryBuilder<ExifInfo, double?, QQueryOperations> exposureSecondsProperty() { |   QueryBuilder<ExifInfo, double?, QQueryOperations> exposureSecondsProperty() { | ||||||
|     return QueryBuilder.apply(this, (query) { |     return QueryBuilder.apply(this, (query) { | ||||||
|       return query.addPropertyName(r'exposureSeconds'); |       return query.addPropertyName(r'exposureSeconds'); | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/ExifResponseDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/ExifResponseDto.md
									
									
									
										generated
									
									
									
								
							| @@ -27,6 +27,7 @@ Name | Type | Description | Notes | |||||||
| **city** | **String** |  | [optional]  | **city** | **String** |  | [optional]  | ||||||
| **state** | **String** |  | [optional]  | **state** | **String** |  | [optional]  | ||||||
| **country** | **String** |  | [optional]  | **country** | **String** |  | [optional]  | ||||||
|  | **description** | **String** |  | [optional]  | ||||||
| 
 | 
 | ||||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								mobile/openapi/doc/UpdateAssetDto.md
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										1
									
								
								mobile/openapi/doc/UpdateAssetDto.md
									
									
									
										generated
									
									
									
								
							| @@ -11,6 +11,7 @@ Name | Type | Description | Notes | |||||||
| **tagIds** | **List<String>** |  | [optional] [default to const []] | **tagIds** | **List<String>** |  | [optional] [default to const []] | ||||||
| **isFavorite** | **bool** |  | [optional]  | **isFavorite** | **bool** |  | [optional]  | ||||||
| **isArchived** | **bool** |  | [optional]  | **isArchived** | **bool** |  | [optional]  | ||||||
|  | **description** | **String** |  | [optional]  | ||||||
| 
 | 
 | ||||||
| [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | [[Back to Model list]](../README.md#documentation-for-models) [[Back to API list]](../README.md#documentation-for-api-endpoints) [[Back to README]](../README.md) | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										17
									
								
								mobile/openapi/lib/model/exif_response_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										17
									
								
								mobile/openapi/lib/model/exif_response_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -32,6 +32,7 @@ class ExifResponseDto { | |||||||
|     this.city, |     this.city, | ||||||
|     this.state, |     this.state, | ||||||
|     this.country, |     this.country, | ||||||
|  |     this.description, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   int? fileSizeInByte; |   int? fileSizeInByte; | ||||||
| @@ -72,6 +73,8 @@ class ExifResponseDto { | |||||||
| 
 | 
 | ||||||
|   String? country; |   String? country; | ||||||
| 
 | 
 | ||||||
|  |   String? description; | ||||||
|  | 
 | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto && |   bool operator ==(Object other) => identical(this, other) || other is ExifResponseDto && | ||||||
|      other.fileSizeInByte == fileSizeInByte && |      other.fileSizeInByte == fileSizeInByte && | ||||||
| @@ -92,7 +95,8 @@ class ExifResponseDto { | |||||||
|      other.longitude == longitude && |      other.longitude == longitude && | ||||||
|      other.city == city && |      other.city == city && | ||||||
|      other.state == state && |      other.state == state && | ||||||
|      other.country == country; |      other.country == country && | ||||||
|  |      other.description == description; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   int get hashCode => |   int get hashCode => | ||||||
| @@ -115,10 +119,11 @@ class ExifResponseDto { | |||||||
|     (longitude == null ? 0 : longitude!.hashCode) + |     (longitude == null ? 0 : longitude!.hashCode) + | ||||||
|     (city == null ? 0 : city!.hashCode) + |     (city == null ? 0 : city!.hashCode) + | ||||||
|     (state == null ? 0 : state!.hashCode) + |     (state == null ? 0 : state!.hashCode) + | ||||||
|     (country == null ? 0 : country!.hashCode); |     (country == null ? 0 : country!.hashCode) + | ||||||
|  |     (description == null ? 0 : description!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, timeZone=$timeZone, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country]'; |   String toString() => 'ExifResponseDto[fileSizeInByte=$fileSizeInByte, make=$make, model=$model, exifImageWidth=$exifImageWidth, exifImageHeight=$exifImageHeight, orientation=$orientation, dateTimeOriginal=$dateTimeOriginal, modifyDate=$modifyDate, timeZone=$timeZone, lensModel=$lensModel, fNumber=$fNumber, focalLength=$focalLength, iso=$iso, exposureTime=$exposureTime, latitude=$latitude, longitude=$longitude, city=$city, state=$state, country=$country, description=$description]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -217,6 +222,11 @@ class ExifResponseDto { | |||||||
|     } else { |     } else { | ||||||
|       // json[r'country'] = null; |       // json[r'country'] = null; | ||||||
|     } |     } | ||||||
|  |     if (this.description != null) { | ||||||
|  |       json[r'description'] = this.description; | ||||||
|  |     } else { | ||||||
|  |       // json[r'description'] = null; | ||||||
|  |     } | ||||||
|     return json; |     return json; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -272,6 +282,7 @@ class ExifResponseDto { | |||||||
|         city: mapValueOfType<String>(json, r'city'), |         city: mapValueOfType<String>(json, r'city'), | ||||||
|         state: mapValueOfType<String>(json, r'state'), |         state: mapValueOfType<String>(json, r'state'), | ||||||
|         country: mapValueOfType<String>(json, r'country'), |         country: mapValueOfType<String>(json, r'country'), | ||||||
|  |         description: mapValueOfType<String>(json, r'description'), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   | |||||||
							
								
								
									
										23
									
								
								mobile/openapi/lib/model/update_asset_dto.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										23
									
								
								mobile/openapi/lib/model/update_asset_dto.dart
									
									
									
										generated
									
									
									
								
							| @@ -16,6 +16,7 @@ class UpdateAssetDto { | |||||||
|     this.tagIds = const [], |     this.tagIds = const [], | ||||||
|     this.isFavorite, |     this.isFavorite, | ||||||
|     this.isArchived, |     this.isArchived, | ||||||
|  |     this.description, | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   List<String> tagIds; |   List<String> tagIds; | ||||||
| @@ -36,21 +37,31 @@ class UpdateAssetDto { | |||||||
|   /// |   /// | ||||||
|   bool? isArchived; |   bool? isArchived; | ||||||
| 
 | 
 | ||||||
|  |   /// | ||||||
|  |   /// Please note: This property should have been non-nullable! Since the specification file | ||||||
|  |   /// does not include a default value (using the "default:" property), however, the generated | ||||||
|  |   /// source code must fall back to having a nullable type. | ||||||
|  |   /// Consider adding a "default:" property in the specification file to hide this note. | ||||||
|  |   /// | ||||||
|  |   String? description; | ||||||
|  | 
 | ||||||
|   @override |   @override | ||||||
|   bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && |   bool operator ==(Object other) => identical(this, other) || other is UpdateAssetDto && | ||||||
|      other.tagIds == tagIds && |      other.tagIds == tagIds && | ||||||
|      other.isFavorite == isFavorite && |      other.isFavorite == isFavorite && | ||||||
|      other.isArchived == isArchived; |      other.isArchived == isArchived && | ||||||
|  |      other.description == description; | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   int get hashCode => |   int get hashCode => | ||||||
|     // ignore: unnecessary_parenthesis |     // ignore: unnecessary_parenthesis | ||||||
|     (tagIds.hashCode) + |     (tagIds.hashCode) + | ||||||
|     (isFavorite == null ? 0 : isFavorite!.hashCode) + |     (isFavorite == null ? 0 : isFavorite!.hashCode) + | ||||||
|     (isArchived == null ? 0 : isArchived!.hashCode); |     (isArchived == null ? 0 : isArchived!.hashCode) + | ||||||
|  |     (description == null ? 0 : description!.hashCode); | ||||||
| 
 | 
 | ||||||
|   @override |   @override | ||||||
|   String toString() => 'UpdateAssetDto[tagIds=$tagIds, isFavorite=$isFavorite, isArchived=$isArchived]'; |   String toString() => 'UpdateAssetDto[tagIds=$tagIds, isFavorite=$isFavorite, isArchived=$isArchived, description=$description]'; | ||||||
| 
 | 
 | ||||||
|   Map<String, dynamic> toJson() { |   Map<String, dynamic> toJson() { | ||||||
|     final json = <String, dynamic>{}; |     final json = <String, dynamic>{}; | ||||||
| @@ -65,6 +76,11 @@ class UpdateAssetDto { | |||||||
|     } else { |     } else { | ||||||
|       // json[r'isArchived'] = null; |       // json[r'isArchived'] = null; | ||||||
|     } |     } | ||||||
|  |     if (this.description != null) { | ||||||
|  |       json[r'description'] = this.description; | ||||||
|  |     } else { | ||||||
|  |       // json[r'description'] = null; | ||||||
|  |     } | ||||||
|     return json; |     return json; | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
| @@ -92,6 +108,7 @@ class UpdateAssetDto { | |||||||
|             : const [], |             : const [], | ||||||
|         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), |         isFavorite: mapValueOfType<bool>(json, r'isFavorite'), | ||||||
|         isArchived: mapValueOfType<bool>(json, r'isArchived'), |         isArchived: mapValueOfType<bool>(json, r'isArchived'), | ||||||
|  |         description: mapValueOfType<String>(json, r'description'), | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     return null; |     return null; | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/exif_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/exif_response_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -111,6 +111,11 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // String description | ||||||
|  |     test('to test the property `description`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   | |||||||
							
								
								
									
										5
									
								
								mobile/openapi/test/update_asset_dto_test.dart
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										5
									
								
								mobile/openapi/test/update_asset_dto_test.dart
									
									
									
										generated
									
									
									
								
							| @@ -31,6 +31,11 @@ void main() { | |||||||
|       // TODO |       // TODO | ||||||
|     }); |     }); | ||||||
| 
 | 
 | ||||||
|  |     // String description | ||||||
|  |     test('to test the property `description`', () async { | ||||||
|  |       // TODO | ||||||
|  |     }); | ||||||
|  | 
 | ||||||
| 
 | 
 | ||||||
|   }); |   }); | ||||||
| 
 | 
 | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| import { SearchPropertiesDto } from './dto/search-properties.dto'; | import { SearchPropertiesDto } from './dto/search-properties.dto'; | ||||||
| import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; | import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto'; | ||||||
| import { AssetEntity, AssetType } from '@app/infra/entities'; | import { AssetEntity, AssetType, ExifEntity } from '@app/infra/entities'; | ||||||
| import { Inject, Injectable } from '@nestjs/common'; | import { Inject, Injectable } from '@nestjs/common'; | ||||||
| import { InjectRepository } from '@nestjs/typeorm'; | import { InjectRepository } from '@nestjs/typeorm'; | ||||||
| import { Repository } from 'typeorm/repository/Repository'; | import { Repository } from 'typeorm/repository/Repository'; | ||||||
| @@ -55,6 +55,7 @@ export class AssetRepository implements IAssetRepository { | |||||||
|     private assetRepository: Repository<AssetEntity>, |     private assetRepository: Repository<AssetEntity>, | ||||||
|  |  | ||||||
|     @Inject(ITagRepository) private _tagRepository: ITagRepository, |     @Inject(ITagRepository) private _tagRepository: ITagRepository, | ||||||
|  |     @InjectRepository(ExifEntity) private exifRepository: Repository<ExifEntity>, | ||||||
|   ) {} |   ) {} | ||||||
|  |  | ||||||
|   async getAllVideos(): Promise<AssetEntity[]> { |   async getAllVideos(): Promise<AssetEntity[]> { | ||||||
| @@ -268,6 +269,17 @@ export class AssetRepository implements IAssetRepository { | |||||||
|       asset.tags = tags; |       asset.tags = tags; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     if (asset.exifInfo != null) { | ||||||
|  |       asset.exifInfo.description = dto.description || ''; | ||||||
|  |       await this.exifRepository.save(asset.exifInfo); | ||||||
|  |     } else { | ||||||
|  |       const exifInfo = new ExifEntity(); | ||||||
|  |       exifInfo.description = dto.description || ''; | ||||||
|  |       exifInfo.asset = asset; | ||||||
|  |       await this.exifRepository.save(exifInfo); | ||||||
|  |       asset.exifInfo = exifInfo; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     return await this.assetRepository.save(asset); |     return await this.assetRepository.save(asset); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; | |||||||
| import { AssetService } from './asset.service'; | import { AssetService } from './asset.service'; | ||||||
| import { AssetController } from './asset.controller'; | import { AssetController } from './asset.controller'; | ||||||
| import { TypeOrmModule } from '@nestjs/typeorm'; | import { TypeOrmModule } from '@nestjs/typeorm'; | ||||||
| import { AssetEntity } from '@app/infra/entities'; | import { AssetEntity, ExifEntity } from '@app/infra/entities'; | ||||||
| import { AssetRepository, IAssetRepository } from './asset-repository'; | import { AssetRepository, IAssetRepository } from './asset-repository'; | ||||||
| import { DownloadModule } from '../../modules/download/download.module'; | import { DownloadModule } from '../../modules/download/download.module'; | ||||||
| import { TagModule } from '../tag/tag.module'; | import { TagModule } from '../tag/tag.module'; | ||||||
| @@ -16,7 +16,7 @@ const ASSET_REPOSITORY_PROVIDER = { | |||||||
| @Module({ | @Module({ | ||||||
|   imports: [ |   imports: [ | ||||||
|     // |     // | ||||||
|     TypeOrmModule.forFeature([AssetEntity]), |     TypeOrmModule.forFeature([AssetEntity, ExifEntity]), | ||||||
|     DownloadModule, |     DownloadModule, | ||||||
|     TagModule, |     TagModule, | ||||||
|     AlbumModule, |     AlbumModule, | ||||||
|   | |||||||
| @@ -25,4 +25,8 @@ export class UpdateAssetDto { | |||||||
|     ], |     ], | ||||||
|   }) |   }) | ||||||
|   tagIds?: string[]; |   tagIds?: string[]; | ||||||
|  |  | ||||||
|  |   @IsOptional() | ||||||
|  |   @IsString() | ||||||
|  |   description?: string; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -223,7 +223,6 @@ export class MetadataExtractionProcessor { | |||||||
|  |  | ||||||
|       const newExif = new ExifEntity(); |       const newExif = new ExifEntity(); | ||||||
|       newExif.assetId = asset.id; |       newExif.assetId = asset.id; | ||||||
|       newExif.description = ''; |  | ||||||
|       newExif.fileSizeInByte = data.format.size || null; |       newExif.fileSizeInByte = data.format.size || null; | ||||||
|       newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null; |       newExif.dateTimeOriginal = fileCreatedAt ? new Date(fileCreatedAt) : null; | ||||||
|       newExif.modifyDate = null; |       newExif.modifyDate = null; | ||||||
|   | |||||||
| @@ -3675,6 +3675,11 @@ | |||||||
|             "type": "string", |             "type": "string", | ||||||
|             "nullable": true, |             "nullable": true, | ||||||
|             "default": null |             "default": null | ||||||
|  |           }, | ||||||
|  |           "description": { | ||||||
|  |             "type": "string", | ||||||
|  |             "nullable": true, | ||||||
|  |             "default": null | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
| @@ -5283,6 +5288,9 @@ | |||||||
|           }, |           }, | ||||||
|           "isArchived": { |           "isArchived": { | ||||||
|             "type": "boolean" |             "type": "boolean" | ||||||
|  |           }, | ||||||
|  |           "description": { | ||||||
|  |             "type": "string" | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|       }, |       }, | ||||||
|   | |||||||
| @@ -23,6 +23,7 @@ export class ExifResponseDto { | |||||||
|   city?: string | null = null; |   city?: string | null = null; | ||||||
|   state?: string | null = null; |   state?: string | null = null; | ||||||
|   country?: string | null = null; |   country?: string | null = null; | ||||||
|  |   description?: string | null = null; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function mapExif(entity: ExifEntity): ExifResponseDto { | export function mapExif(entity: ExifEntity): ExifResponseDto { | ||||||
| @@ -46,5 +47,6 @@ export function mapExif(entity: ExifEntity): ExifResponseDto { | |||||||
|     city: entity.city, |     city: entity.city, | ||||||
|     state: entity.state, |     state: entity.state, | ||||||
|     country: entity.country, |     country: entity.country, | ||||||
|  |     description: entity.description, | ||||||
|   }; |   }; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -343,6 +343,7 @@ const assetInfo: ExifResponseDto = { | |||||||
|   city: 'city', |   city: 'city', | ||||||
|   state: 'state', |   state: 'state', | ||||||
|   country: 'country', |   country: 'country', | ||||||
|  |   description: 'description', | ||||||
| }; | }; | ||||||
|  |  | ||||||
| const assetResponse: AssetResponseDto = { | const assetResponse: AssetResponseDto = { | ||||||
|   | |||||||
							
								
								
									
										12
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										12
									
								
								web/src/api/open-api/api.ts
									
									
									
										generated
									
									
									
								
							| @@ -1208,6 +1208,12 @@ export interface ExifResponseDto { | |||||||
|      * @memberof ExifResponseDto |      * @memberof ExifResponseDto | ||||||
|      */ |      */ | ||||||
|     'country'?: string | null; |     'country'?: string | null; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {string} | ||||||
|  |      * @memberof ExifResponseDto | ||||||
|  |      */ | ||||||
|  |     'description'?: string | null; | ||||||
| } | } | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
| @@ -2341,6 +2347,12 @@ export interface UpdateAssetDto { | |||||||
|      * @memberof UpdateAssetDto |      * @memberof UpdateAssetDto | ||||||
|      */ |      */ | ||||||
|     'isArchived'?: boolean; |     'isArchived'?: boolean; | ||||||
|  |     /** | ||||||
|  |      *  | ||||||
|  |      * @type {string} | ||||||
|  |      * @memberof UpdateAssetDto | ||||||
|  |      */ | ||||||
|  |     'description'?: string; | ||||||
| } | } | ||||||
| /** | /** | ||||||
|  *  |  *  | ||||||
|   | |||||||
| @@ -251,6 +251,18 @@ | |||||||
| 			} | 			} | ||||||
| 		}); | 		}); | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
|  | 	const disableKeyDownEvent = () => { | ||||||
|  | 		if (browser) { | ||||||
|  | 			document.removeEventListener('keydown', onKeyboardPress); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const enableKeyDownEvent = () => { | ||||||
|  | 		if (browser) { | ||||||
|  | 			document.addEventListener('keydown', onKeyboardPress); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section | <section | ||||||
| @@ -352,7 +364,13 @@ | |||||||
| 			class="bg-immich-bg w-[360px] row-span-full transition-all overflow-y-auto dark:bg-immich-dark-bg dark:border-l dark:border-l-immich-dark-gray" | 			class="bg-immich-bg w-[360px] row-span-full transition-all overflow-y-auto dark:bg-immich-dark-bg dark:border-l dark:border-l-immich-dark-gray" | ||||||
| 			translate="yes" | 			translate="yes" | ||||||
| 		> | 		> | ||||||
| 			<DetailPanel {asset} albums={appearsInAlbums} on:close={() => (isShowDetail = false)} /> | 			<DetailPanel | ||||||
|  | 				{asset} | ||||||
|  | 				albums={appearsInAlbums} | ||||||
|  | 				on:close={() => (isShowDetail = false)} | ||||||
|  | 				on:description-focus-in={disableKeyDownEvent} | ||||||
|  | 				on:description-focus-out={enableKeyDownEvent} | ||||||
|  | 			/> | ||||||
| 		</div> | 		</div> | ||||||
| 	{/if} | 	{/if} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -5,14 +5,26 @@ | |||||||
| 	import CameraIris from 'svelte-material-icons/CameraIris.svelte'; | 	import CameraIris from 'svelte-material-icons/CameraIris.svelte'; | ||||||
| 	import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte'; | 	import MapMarkerOutline from 'svelte-material-icons/MapMarkerOutline.svelte'; | ||||||
| 	import { createEventDispatcher } from 'svelte'; | 	import { createEventDispatcher } from 'svelte'; | ||||||
| 	import { AssetResponseDto, AlbumResponseDto } from '@api'; | 	import { AssetResponseDto, AlbumResponseDto, api } from '@api'; | ||||||
| 	import { asByteUnitString } from '../../utils/byte-units'; | 	import { asByteUnitString } from '../../utils/byte-units'; | ||||||
| 	import { locale } from '$lib/stores/preferences.store'; | 	import { locale } from '$lib/stores/preferences.store'; | ||||||
| 	import { DateTime } from 'luxon'; | 	import { DateTime } from 'luxon'; | ||||||
| 	import type { LatLngTuple } from 'leaflet'; | 	import type { LatLngTuple } from 'leaflet'; | ||||||
|  | 	import { page } from '$app/stores'; | ||||||
|  |  | ||||||
| 	export let asset: AssetResponseDto; | 	export let asset: AssetResponseDto; | ||||||
| 	export let albums: AlbumResponseDto[] = []; | 	export let albums: AlbumResponseDto[] = []; | ||||||
|  | 	let textarea: HTMLTextAreaElement; | ||||||
|  | 	let description: string; | ||||||
|  |  | ||||||
|  | 	$: { | ||||||
|  | 		// Get latest description from server | ||||||
|  | 		if (asset.id) { | ||||||
|  | 			api.assetApi | ||||||
|  | 				.getAssetById(asset.id) | ||||||
|  | 				.then((res) => (textarea.value = res.data?.exifInfo?.description || '')); | ||||||
|  | 		} | ||||||
|  | 	} | ||||||
|  |  | ||||||
| 	$: latlng = (() => { | 	$: latlng = (() => { | ||||||
| 		const lat = asset.exifInfo?.latitude; | 		const lat = asset.exifInfo?.latitude; | ||||||
| @@ -34,6 +46,27 @@ | |||||||
|  |  | ||||||
| 		return undefined; | 		return undefined; | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
|  | 	const autoGrowHeight = (e: Event) => { | ||||||
|  | 		const target = e.target as HTMLTextAreaElement; | ||||||
|  | 		target.style.height = 'auto'; | ||||||
|  | 		target.style.height = `${target.scrollHeight}px`; | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const handleFocusIn = () => { | ||||||
|  | 		dispatch('description-focus-in'); | ||||||
|  | 	}; | ||||||
|  |  | ||||||
|  | 	const handleFocusOut = async () => { | ||||||
|  | 		dispatch('description-focus-out'); | ||||||
|  | 		try { | ||||||
|  | 			await api.assetApi.updateAsset(asset.id, { | ||||||
|  | 				description: description | ||||||
|  | 			}); | ||||||
|  | 		} catch (error) { | ||||||
|  | 			console.error(error); | ||||||
|  | 		} | ||||||
|  | 	}; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
| <section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> | <section class="p-2 dark:bg-immich-dark-bg dark:text-immich-dark-fg"> | ||||||
| @@ -48,6 +81,23 @@ | |||||||
| 		<p class="text-immich-fg dark:text-immich-dark-fg text-lg">Info</p> | 		<p class="text-immich-fg dark:text-immich-dark-fg text-lg">Info</p> | ||||||
| 	</div> | 	</div> | ||||||
|  |  | ||||||
|  | 	<div class="mx-4 mt-10"> | ||||||
|  | 		<textarea | ||||||
|  | 			bind:this={textarea} | ||||||
|  | 			class="max-h-[500px] | ||||||
|  |       text-base text-black bg-transparent dark:text-white border-b focus:border-b-2 border-gray-500 w-full focus:border-immich-primary dark:focus:border-immich-dark-primary transition-all resize-none overflow-hidden outline-none disabled:border-none" | ||||||
|  | 			placeholder={$page?.data?.user?.id !== asset.ownerId ? '' : 'Add a description'} | ||||||
|  | 			style:display={$page?.data?.user?.id !== asset.ownerId && textarea?.value == '' | ||||||
|  | 				? 'none' | ||||||
|  | 				: 'block'} | ||||||
|  | 			on:focusin={handleFocusIn} | ||||||
|  | 			on:focusout={handleFocusOut} | ||||||
|  | 			on:input={autoGrowHeight} | ||||||
|  | 			bind:value={description} | ||||||
|  | 			disabled={$page?.data?.user?.id !== asset.ownerId} | ||||||
|  | 		/> | ||||||
|  | 	</div> | ||||||
|  |  | ||||||
| 	<div class="px-4 py-4"> | 	<div class="px-4 py-4"> | ||||||
| 		{#if !asset.exifInfo} | 		{#if !asset.exifInfo} | ||||||
| 			<p class="text-sm pb-4">NO EXIF INFO AVAILABLE</p> | 			<p class="text-sm pb-4">NO EXIF INFO AVAILABLE</p> | ||||||
| @@ -178,7 +228,7 @@ | |||||||
| <section class="p-2 dark:text-immich-dark-fg"> | <section class="p-2 dark:text-immich-dark-fg"> | ||||||
| 	<div class="px-4 py-4"> | 	<div class="px-4 py-4"> | ||||||
| 		{#if albums.length > 0} | 		{#if albums.length > 0} | ||||||
| 			<p class="text-sm pb-4 ">APPEARS IN</p> | 			<p class="text-sm pb-4">APPEARS IN</p> | ||||||
| 		{/if} | 		{/if} | ||||||
| 		{#each albums as album} | 		{#each albums as album} | ||||||
| 			<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}> | 			<a data-sveltekit-preload-data="hover" href={`/albums/${album.id}`}> | ||||||
|   | |||||||
| @@ -24,29 +24,50 @@ | |||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const getFavoriteCount = async () => { | 	const getFavoriteCount = async () => { | ||||||
| 		const { data: assets } = await api.assetApi.getAllAssets(true, undefined); | 		try { | ||||||
|  | 			const { data: assets } = await api.assetApi.getAllAssets(true, undefined); | ||||||
|  |  | ||||||
| 		return { | 			return { | ||||||
| 			favorites: assets.length | 				favorites: assets.length | ||||||
| 		}; | 			}; | ||||||
|  | 		} catch { | ||||||
|  | 			return { | ||||||
|  | 				favorites: 0 | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const getAlbumCount = async () => { | 	const getAlbumCount = async () => { | ||||||
| 		const { data: albumCount } = await api.albumApi.getAlbumCountByUserId(); | 		try { | ||||||
| 		return { | 			const { data: albumCount } = await api.albumApi.getAlbumCountByUserId(); | ||||||
| 			shared: albumCount.shared, | 			return { | ||||||
| 			sharing: albumCount.sharing, | 				shared: albumCount.shared, | ||||||
| 			owned: albumCount.owned | 				sharing: albumCount.sharing, | ||||||
| 		}; | 				owned: albumCount.owned | ||||||
|  | 			}; | ||||||
|  | 		} catch { | ||||||
|  | 			return { | ||||||
|  | 				shared: 0, | ||||||
|  | 				sharing: 0, | ||||||
|  | 				owned: 0 | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
| 	}; | 	}; | ||||||
|  |  | ||||||
| 	const getArchivedAssetsCount = async () => { | 	const getArchivedAssetsCount = async () => { | ||||||
| 		const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId(); | 		try { | ||||||
|  | 			const { data: assetCount } = await api.assetApi.getArchivedAssetCountByUserId(); | ||||||
|  |  | ||||||
| 		return { | 			return { | ||||||
| 			videos: assetCount.videos, | 				videos: assetCount.videos, | ||||||
| 			photos: assetCount.photos | 				photos: assetCount.photos | ||||||
| 		}; | 			}; | ||||||
|  | 		} catch { | ||||||
|  | 			return { | ||||||
|  | 				videos: 0, | ||||||
|  | 				photos: 0 | ||||||
|  | 			}; | ||||||
|  | 		} | ||||||
| 	}; | 	}; | ||||||
| </script> | </script> | ||||||
|  |  | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user