Aswanth Alakkadan 2025-12-16 12:11:55 -06:00 committed by GitHub
commit dd9dbac254
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
9 changed files with 68 additions and 1 deletions

View File

@ -22387,6 +22387,14 @@
}, },
"type": "array" "type": "array"
}, },
"originalFileName": {
"description": "Array of original file names for each asset",
"items": {
"nullable": true,
"type": "string"
},
"type": "array"
},
"ownerId": { "ownerId": {
"description": "Array of owner IDs for each asset", "description": "Array of owner IDs for each asset",
"items": { "items": {
@ -22449,6 +22457,7 @@
"isTrashed", "isTrashed",
"livePhotoVideoId", "livePhotoVideoId",
"localOffsetHours", "localOffsetHours",
"originalFileName",
"ownerId", "ownerId",
"projectionType", "projectionType",
"ratio", "ratio",

View File

@ -207,6 +207,13 @@ export class TimeBucketAssetResponseDto {
description: 'Array of longitude coordinates extracted from EXIF GPS data', description: 'Array of longitude coordinates extracted from EXIF GPS data',
}) })
longitude!: number[]; longitude!: number[];
@ApiProperty({
type: 'array',
items: { type: 'string', nullable: true },
description: 'Array of original file names for each asset',
})
originalFileName!: (string | null)[];
} }
export class TimeBucketsResponseDto { export class TimeBucketsResponseDto {

View File

@ -641,6 +641,7 @@ export class AssetRepository {
eb.lit(1), eb.lit(1),
) )
.as('ratio'), .as('ratio'),
'asset.originalFileName',
]) ])
.$if(!!options.withCoordinates, (qb) => qb.select(['asset_exif.latitude', 'asset_exif.longitude'])) .$if(!!options.withCoordinates, (qb) => qb.select(['asset_exif.latitude', 'asset_exif.longitude']))
.where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null) .where('asset.deletedAt', options.isTrashed ? 'is not' : 'is', null)
@ -715,6 +716,7 @@ export class AssetRepository {
eb.fn.coalesce(eb.fn('array_agg', ['ratio']), sql.lit('{}')).as('ratio'), eb.fn.coalesce(eb.fn('array_agg', ['ratio']), sql.lit('{}')).as('ratio'),
eb.fn.coalesce(eb.fn('array_agg', ['status']), sql.lit('{}')).as('status'), eb.fn.coalesce(eb.fn('array_agg', ['status']), sql.lit('{}')).as('status'),
eb.fn.coalesce(eb.fn('array_agg', ['thumbhash']), sql.lit('{}')).as('thumbhash'), eb.fn.coalesce(eb.fn('array_agg', ['thumbhash']), sql.lit('{}')).as('thumbhash'),
eb.fn.coalesce(eb.fn('array_agg', ['originalFileName']), sql.lit('{}')).as('originalFileName'),
]) ])
.$if(!!options.withCoordinates, (qb) => .$if(!!options.withCoordinates, (qb) =>
qb.select((eb) => [ qb.select((eb) => [

View File

@ -30,7 +30,7 @@
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetJobName, getSharedLink } from '$lib/utils'; import { getAssetJobName, getSharedLink } from '$lib/utils';
import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { canCopyImageToClipboard, isDngFile } from '$lib/utils/asset-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import { import {
AssetJobName, AssetJobName,
@ -109,6 +109,7 @@
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline); let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
let isLocked = $derived(asset.visibility === AssetVisibility.Locked); let isLocked = $derived(asset.visibility === AssetVisibility.Locked);
let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch); let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
let isRawFile = $derived(asset.type === AssetTypeEnum.Image && isDngFile(asset));
// $: showEditorButton = // $: showEditorButton =
// isOwner && // isOwner &&
@ -132,6 +133,17 @@
<div class="flex gap-2 overflow-x-auto dark" data-testid="asset-viewer-navbar-actions"> <div class="flex gap-2 overflow-x-auto dark" data-testid="asset-viewer-navbar-actions">
<CastButton /> <CastButton />
{#if isRawFile}
<div
class="flex place-items-center text-sm font-semibold text-white pointer-events-auto"
title="RAW files preserves more image details and provides greater control over editing adjustments like exposure, white balance, and color grading"
>
<span class="px-3 py-1.5">
RAW
</span>
</div>
{/if}
{#if !asset.isTrashed && $user && !isLocked} {#if !asset.isTrashed && $user && !isLocked}
<ShareAction {asset} /> <ShareAction {asset} />
{/if} {/if}

View File

@ -2,6 +2,7 @@
import { ProjectionType } from '$lib/constants'; import { ProjectionType } from '$lib/constants';
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store'; import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils'; import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
import { isDngFile } from '$lib/utils/asset-utils';
import { timeToSeconds } from '$lib/utils/date-time'; import { timeToSeconds } from '$lib/utils/date-time';
import { getAltText } from '$lib/utils/thumbnail-util'; import { getAltText } from '$lib/utils/thumbnail-util';
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk'; import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
@ -303,6 +304,12 @@
</div> </div>
{/if} {/if}
{#if asset.isImage && isDngFile(asset)}
<div class="absolute end-2 top-2 flex place-items-center gap-1 text-xs font-semibold text-white">
<span class="px-2 py-1 ">RAW</span>
</div>
{/if}
<!-- Stacked asset --> <!-- Stacked asset -->
{#if asset.stack && showStackedIcon} {#if asset.stack && showStackedIcon}
<div <div

View File

@ -190,6 +190,7 @@ export class MonthGroup {
: null, : null,
thumbhash: bucketAssets.thumbhash[i], thumbhash: bucketAssets.thumbhash[i],
people: null, // People are not included in the bucket assets people: null, // People are not included in the bucket assets
originalFileName: bucketAssets.originalFileName?.[i] || null,
}; };
if (bucketAssets.latitude?.[i] && bucketAssets.longitude?.[i]) { if (bucketAssets.latitude?.[i] && bucketAssets.longitude?.[i]) {

View File

@ -35,6 +35,8 @@ export type TimelineAsset = {
people: string[] | null; people: string[] | null;
latitude?: number | null; latitude?: number | null;
longitude?: number | null; longitude?: number | null;
originalFileName?: string | null;
originalMimeType?: string | null;
}; };
export type MoveAsset = { asset: TimelineAsset; date: TimelineDate }; export type MoveAsset = { asset: TimelineAsset; date: TimelineDate };

View File

@ -367,6 +367,31 @@ export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
return supportedImageMimeTypes.has(asset.originalMimeType); return supportedImageMimeTypes.has(asset.originalMimeType);
} }
/**
* Returns true if the asset is a DNG file, false otherwise
* Checks both file extension and mime type
*
* Note: Assets loaded from time buckets don't include originalMimeType or originalFileName.
* To show RAW tags for all DNG files, add originalMimeType to TimeBucketAssetResponseDto on the server.
*/
export function isDngFile(asset: { originalFileName?: string | null; originalMimeType?: string | null }): boolean {
// Check by mime type first (more reliable)
if (asset.originalMimeType) {
const dngMimeTypes = ['image/dng', 'image/x-adobe-dng'];
if (dngMimeTypes.includes(asset.originalMimeType.toLowerCase())) {
return true;
}
}
// Fallback to file extension
if (asset.originalFileName) {
const extension = getFilenameExtension(asset.originalFileName);
return extension === 'dng';
}
return false;
}
export const getAssetType = (type: AssetTypeEnum) => { export const getAssetType = (type: AssetTypeEnum) => {
switch (type) { switch (type) {
case 'IMAGE': { case 'IMAGE': {

View File

@ -189,6 +189,8 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
people, people,
latitude: assetResponse.exifInfo?.latitude || null, latitude: assetResponse.exifInfo?.latitude || null,
longitude: assetResponse.exifInfo?.longitude || null, longitude: assetResponse.exifInfo?.longitude || null,
originalFileName: assetResponse.originalFileName || null,
originalMimeType: assetResponse.originalMimeType || null,
}; };
}; };