From b63d6cdcd638061855788d046ac89b32a11cc88f Mon Sep 17 00:00:00 2001 From: koostamas Date: Sat, 17 May 2025 12:17:00 +0200 Subject: [PATCH] feat: bulk change description (#18288) Co-authored-by: Tamas Koos --- i18n/en.json | 4 ++ .../lib/model/asset_bulk_update_dto.dart | 19 +++++++++- open-api/immich-openapi-specs.json | 3 ++ open-api/typescript-sdk/src/fetch-client.ts | 1 + server/src/dtos/asset.dto.ts | 8 ++-- server/src/services/asset.service.ts | 16 ++++++-- .../memory-page/memory-viewer.svelte | 2 + .../actions/change-description-action.svelte | 37 +++++++++++++++++++ .../AssetUpdateDecriptionConfirmModal.svelte | 29 +++++++++++++++ .../[[assetId=id]]/+page.svelte | 2 + .../[[assetId=id]]/+page.svelte | 2 + .../[[assetId=id]]/+page.svelte | 2 + .../[[assetId=id]]/+page.svelte | 2 + .../(user)/photos/[[assetId=id]]/+page.svelte | 2 + .../[[assetId=id]]/+page.svelte | 2 + 15 files changed, 122 insertions(+), 9 deletions(-) create mode 100644 web/src/lib/components/photos-page/actions/change-description-action.svelte create mode 100644 web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte diff --git a/i18n/en.json b/i18n/en.json index 578fe9a115..b9331df5db 100644 --- a/i18n/en.json +++ b/i18n/en.json @@ -601,6 +601,7 @@ "cannot_undo_this_action": "You cannot undo this action!", "cannot_update_the_description": "Cannot update the description", "change_date": "Change date", + "change_description": "Change description", "change_display_order": "Change display order", "change_expiration_time": "Change expiration time", "change_location": "Change location", @@ -794,6 +795,8 @@ "edit_avatar": "Edit avatar", "edit_date": "Edit date", "edit_date_and_time": "Edit date and time", + "edit_description": "Edit description", + "edit_description_prompt": "Please select a new description:", "edit_exclusion_pattern": "Edit exclusion pattern", "edit_faces": "Edit faces", "edit_import_path": "Edit import path", @@ -882,6 +885,7 @@ "unable_to_archive_unarchive": "Unable to {archived, select, true {archive} other {unarchive}}", "unable_to_change_album_user_role": "Unable to change the album user's role", "unable_to_change_date": "Unable to change date", + "unable_to_change_description": "Unable to change description", "unable_to_change_favorite": "Unable to change favorite for asset", "unable_to_change_location": "Unable to change location", "unable_to_change_password": "Unable to change password", diff --git a/mobile/openapi/lib/model/asset_bulk_update_dto.dart b/mobile/openapi/lib/model/asset_bulk_update_dto.dart index 39d7cd996f..571badf029 100644 --- a/mobile/openapi/lib/model/asset_bulk_update_dto.dart +++ b/mobile/openapi/lib/model/asset_bulk_update_dto.dart @@ -14,6 +14,7 @@ class AssetBulkUpdateDto { /// Returns a new [AssetBulkUpdateDto] instance. AssetBulkUpdateDto({ this.dateTimeOriginal, + this.description, this.duplicateId, this.ids = const [], this.isFavorite, @@ -31,6 +32,14 @@ class AssetBulkUpdateDto { /// String? dateTimeOriginal; + /// + /// 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; + String? duplicateId; List ids; @@ -80,6 +89,7 @@ class AssetBulkUpdateDto { @override bool operator ==(Object other) => identical(this, other) || other is AssetBulkUpdateDto && other.dateTimeOriginal == dateTimeOriginal && + other.description == description && other.duplicateId == duplicateId && _deepEquality.equals(other.ids, ids) && other.isFavorite == isFavorite && @@ -92,6 +102,7 @@ class AssetBulkUpdateDto { int get hashCode => // ignore: unnecessary_parenthesis (dateTimeOriginal == null ? 0 : dateTimeOriginal!.hashCode) + + (description == null ? 0 : description!.hashCode) + (duplicateId == null ? 0 : duplicateId!.hashCode) + (ids.hashCode) + (isFavorite == null ? 0 : isFavorite!.hashCode) + @@ -101,7 +112,7 @@ class AssetBulkUpdateDto { (visibility == null ? 0 : visibility!.hashCode); @override - String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]'; + String toString() => 'AssetBulkUpdateDto[dateTimeOriginal=$dateTimeOriginal, description=$description, duplicateId=$duplicateId, ids=$ids, isFavorite=$isFavorite, latitude=$latitude, longitude=$longitude, rating=$rating, visibility=$visibility]'; Map toJson() { final json = {}; @@ -110,6 +121,11 @@ class AssetBulkUpdateDto { } else { // json[r'dateTimeOriginal'] = null; } + if (this.description != null) { + json[r'description'] = this.description; + } else { + // json[r'description'] = null; + } if (this.duplicateId != null) { json[r'duplicateId'] = this.duplicateId; } else { @@ -154,6 +170,7 @@ class AssetBulkUpdateDto { return AssetBulkUpdateDto( dateTimeOriginal: mapValueOfType(json, r'dateTimeOriginal'), + description: mapValueOfType(json, r'description'), duplicateId: mapValueOfType(json, r'duplicateId'), ids: json[r'ids'] is Iterable ? (json[r'ids'] as Iterable).cast().toList(growable: false) diff --git a/open-api/immich-openapi-specs.json b/open-api/immich-openapi-specs.json index e7bf81ce3e..5de3987367 100644 --- a/open-api/immich-openapi-specs.json +++ b/open-api/immich-openapi-specs.json @@ -8605,6 +8605,9 @@ "dateTimeOriginal": { "type": "string" }, + "description": { + "type": "string" + }, "duplicateId": { "nullable": true, "type": "string" diff --git a/open-api/typescript-sdk/src/fetch-client.ts b/open-api/typescript-sdk/src/fetch-client.ts index 1d3a04da44..c293b2aa6c 100644 --- a/open-api/typescript-sdk/src/fetch-client.ts +++ b/open-api/typescript-sdk/src/fetch-client.ts @@ -431,6 +431,7 @@ export type AssetMediaResponseDto = { }; export type AssetBulkUpdateDto = { dateTimeOriginal?: string; + description?: string; duplicateId?: string | null; ids: string[]; isFavorite?: boolean; diff --git a/server/src/dtos/asset.dto.ts b/server/src/dtos/asset.dto.ts index 0789633878..940cfbf9cc 100644 --- a/server/src/dtos/asset.dto.ts +++ b/server/src/dtos/asset.dto.ts @@ -54,6 +54,10 @@ export class UpdateAssetBase { @Max(5) @Min(-1) rating?: number; + + @Optional() + @IsString() + description?: string; } export class AssetBulkUpdateDto extends UpdateAssetBase { @@ -65,10 +69,6 @@ export class AssetBulkUpdateDto extends UpdateAssetBase { } export class UpdateAssetDto extends UpdateAssetBase { - @Optional() - @IsString() - description?: string; - @ValidateUUID({ optional: true, nullable: true }) livePhotoVideoId?: string | null; } diff --git a/server/src/services/asset.service.ts b/server/src/services/asset.service.ts index 556641fdb0..bc73ff6410 100644 --- a/server/src/services/asset.service.ts +++ b/server/src/services/asset.service.ts @@ -108,13 +108,21 @@ export class AssetService extends BaseService { } async updateAll(auth: AuthDto, dto: AssetBulkUpdateDto): Promise { - const { ids, dateTimeOriginal, latitude, longitude, ...options } = dto; + const { ids, description, dateTimeOriginal, latitude, longitude, ...options } = dto; await this.requireAccess({ auth, permission: Permission.ASSET_UPDATE, ids }); - if (dateTimeOriginal !== undefined || latitude !== undefined || longitude !== undefined) { - await this.assetRepository.updateAllExif(ids, { dateTimeOriginal, latitude, longitude }); + if ( + description !== undefined || + dateTimeOriginal !== undefined || + latitude !== undefined || + longitude !== undefined + ) { + await this.assetRepository.updateAllExif(ids, { description, dateTimeOriginal, latitude, longitude }); await this.jobRepository.queueAll( - ids.map((id) => ({ name: JobName.SIDECAR_WRITE, data: { id, dateTimeOriginal, latitude, longitude } })), + ids.map((id) => ({ + name: JobName.SIDECAR_WRITE, + data: { id, description, dateTimeOriginal, latitude, longitude }, + })), ); } diff --git a/web/src/lib/components/memory-page/memory-viewer.svelte b/web/src/lib/components/memory-page/memory-viewer.svelte index 09218d3c47..468fbe6d41 100644 --- a/web/src/lib/components/memory-page/memory-viewer.svelte +++ b/web/src/lib/components/memory-page/memory-viewer.svelte @@ -8,6 +8,7 @@ import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; + import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; @@ -323,6 +324,7 @@ + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} diff --git a/web/src/lib/components/photos-page/actions/change-description-action.svelte b/web/src/lib/components/photos-page/actions/change-description-action.svelte new file mode 100644 index 0000000000..129d327fb9 --- /dev/null +++ b/web/src/lib/components/photos-page/actions/change-description-action.svelte @@ -0,0 +1,37 @@ + + +{#if menuItem} + handleUpdateDescription()} /> +{/if} diff --git a/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte b/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte new file mode 100644 index 0000000000..4d5a81f5fa --- /dev/null +++ b/web/src/lib/modals/AssetUpdateDecriptionConfirmModal.svelte @@ -0,0 +1,29 @@ + + + (confirmed ? onClose(description) : onClose())} +> + {#snippet promptSnippet()} +
+
+ + +
+
+ {/snippet} +
diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 088d3dae97..e46ad0fc77 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -13,6 +13,7 @@ import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; + import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; @@ -478,6 +479,7 @@ {#if assetInteraction.isAllUserOwned} + {#if assetInteraction.selectedAssets.length === 1} + + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned} diff --git a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte index 1dc213729d..ea726d783a 100644 --- a/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/people/[personId]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -11,6 +11,7 @@ import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte'; import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; + import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; @@ -515,6 +516,7 @@ onClick={handleReassignAssets} /> + {/if} + assetStore.removeAssets(assetIds)} /> {#if $preferences.tags.enabled} diff --git a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte index 5f995b9a7a..813683244e 100644 --- a/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/search/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -9,6 +9,7 @@ import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte'; import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte'; import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte'; + import ChangeDescription from '$lib/components/photos-page/actions/change-description-action.svelte'; import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte'; import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte'; import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte'; @@ -358,6 +359,7 @@ + {#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}