Merge b65544cc83 into f0b069adb9
commit
dd9dbac254
|
|
@ -22387,6 +22387,14 @@
|
|||
},
|
||||
"type": "array"
|
||||
},
|
||||
"originalFileName": {
|
||||
"description": "Array of original file names for each asset",
|
||||
"items": {
|
||||
"nullable": true,
|
||||
"type": "string"
|
||||
},
|
||||
"type": "array"
|
||||
},
|
||||
"ownerId": {
|
||||
"description": "Array of owner IDs for each asset",
|
||||
"items": {
|
||||
|
|
@ -22449,6 +22457,7 @@
|
|||
"isTrashed",
|
||||
"livePhotoVideoId",
|
||||
"localOffsetHours",
|
||||
"originalFileName",
|
||||
"ownerId",
|
||||
"projectionType",
|
||||
"ratio",
|
||||
|
|
|
|||
|
|
@ -207,6 +207,13 @@ export class TimeBucketAssetResponseDto {
|
|||
description: 'Array of longitude coordinates extracted from EXIF GPS data',
|
||||
})
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -641,6 +641,7 @@ export class AssetRepository {
|
|||
eb.lit(1),
|
||||
)
|
||||
.as('ratio'),
|
||||
'asset.originalFileName',
|
||||
])
|
||||
.$if(!!options.withCoordinates, (qb) => qb.select(['asset_exif.latitude', 'asset_exif.longitude']))
|
||||
.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', ['status']), sql.lit('{}')).as('status'),
|
||||
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) =>
|
||||
qb.select((eb) => [
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@
|
|||
import { user } from '$lib/stores/user.store';
|
||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
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 {
|
||||
AssetJobName,
|
||||
|
|
@ -109,6 +109,7 @@
|
|||
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
||||
let isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
||||
let smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
||||
let isRawFile = $derived(asset.type === AssetTypeEnum.Image && isDngFile(asset));
|
||||
|
||||
// $: showEditorButton =
|
||||
// isOwner &&
|
||||
|
|
@ -132,6 +133,17 @@
|
|||
<div class="flex gap-2 overflow-x-auto dark" data-testid="asset-viewer-navbar-actions">
|
||||
<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}
|
||||
<ShareAction {asset} />
|
||||
{/if}
|
||||
|
|
|
|||
|
|
@ -2,6 +2,7 @@
|
|||
import { ProjectionType } from '$lib/constants';
|
||||
import { locale, playVideoThumbnailOnHover } from '$lib/stores/preferences.store';
|
||||
import { getAssetOriginalUrl, getAssetPlaybackUrl, getAssetThumbnailUrl } from '$lib/utils';
|
||||
import { isDngFile } from '$lib/utils/asset-utils';
|
||||
import { timeToSeconds } from '$lib/utils/date-time';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { AssetMediaSize, AssetVisibility, type UserResponseDto } from '@immich/sdk';
|
||||
|
|
@ -303,6 +304,12 @@
|
|||
</div>
|
||||
{/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 -->
|
||||
{#if asset.stack && showStackedIcon}
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -190,6 +190,7 @@ export class MonthGroup {
|
|||
: null,
|
||||
thumbhash: bucketAssets.thumbhash[i],
|
||||
people: null, // People are not included in the bucket assets
|
||||
originalFileName: bucketAssets.originalFileName?.[i] || null,
|
||||
};
|
||||
|
||||
if (bucketAssets.latitude?.[i] && bucketAssets.longitude?.[i]) {
|
||||
|
|
|
|||
|
|
@ -35,6 +35,8 @@ export type TimelineAsset = {
|
|||
people: string[] | null;
|
||||
latitude?: number | null;
|
||||
longitude?: number | null;
|
||||
originalFileName?: string | null;
|
||||
originalMimeType?: string | null;
|
||||
};
|
||||
|
||||
export type MoveAsset = { asset: TimelineAsset; date: TimelineDate };
|
||||
|
|
|
|||
|
|
@ -367,6 +367,31 @@ export function isWebCompatibleImage(asset: AssetResponseDto): boolean {
|
|||
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) => {
|
||||
switch (type) {
|
||||
case 'IMAGE': {
|
||||
|
|
|
|||
|
|
@ -189,6 +189,8 @@ export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset):
|
|||
people,
|
||||
latitude: assetResponse.exifInfo?.latitude || null,
|
||||
longitude: assetResponse.exifInfo?.longitude || null,
|
||||
originalFileName: assetResponse.originalFileName || null,
|
||||
originalMimeType: assetResponse.originalMimeType || null,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue