feat(web): always view original of animated images (#23842)
parent
271a42ac7f
commit
76c73549ae
|
|
@ -1,7 +1,7 @@
|
||||||
import { getAnimateMock } from '$lib/__mocks__/animate.mock';
|
import { getAnimateMock } from '$lib/__mocks__/animate.mock';
|
||||||
import PhotoViewer from '$lib/components/asset-viewer/photo-viewer.svelte';
|
import PhotoViewer from '$lib/components/asset-viewer/photo-viewer.svelte';
|
||||||
import * as utils from '$lib/utils';
|
import * as utils from '$lib/utils';
|
||||||
import { AssetMediaSize } from '@immich/sdk';
|
import { AssetMediaSize, AssetTypeEnum } from '@immich/sdk';
|
||||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||||
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
import { sharedLinkFactory } from '@test-data/factories/shared-link-factory';
|
||||||
import { render } from '@testing-library/svelte';
|
import { render } from '@testing-library/svelte';
|
||||||
|
|
@ -65,7 +65,11 @@ describe('PhotoViewer component', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the thumbnail', () => {
|
it('loads the thumbnail', () => {
|
||||||
const asset = assetFactory.build({ originalPath: 'image.jpg', originalMimeType: 'image/jpeg' });
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.jpg',
|
||||||
|
originalMimeType: 'image/jpeg',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
});
|
||||||
render(PhotoViewer, { asset });
|
render(PhotoViewer, { asset });
|
||||||
|
|
||||||
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
||||||
|
|
@ -76,16 +80,89 @@ describe('PhotoViewer component', () => {
|
||||||
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads the original image for gifs', () => {
|
it('loads the thumbnail image for static gifs', () => {
|
||||||
const asset = assetFactory.build({ originalPath: 'image.gif', originalMimeType: 'image/gif' });
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.gif',
|
||||||
|
originalMimeType: 'image/gif',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
});
|
||||||
|
render(PhotoViewer, { asset });
|
||||||
|
|
||||||
|
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
||||||
|
id: asset.id,
|
||||||
|
size: AssetMediaSize.Preview,
|
||||||
|
cacheKey: asset.thumbhash,
|
||||||
|
});
|
||||||
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the thumbnail image for static webp images', () => {
|
||||||
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.webp',
|
||||||
|
originalMimeType: 'image/webp',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
});
|
||||||
|
render(PhotoViewer, { asset });
|
||||||
|
|
||||||
|
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
||||||
|
id: asset.id,
|
||||||
|
size: AssetMediaSize.Preview,
|
||||||
|
cacheKey: asset.thumbhash,
|
||||||
|
});
|
||||||
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads the original image for animated gifs', () => {
|
||||||
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.gif',
|
||||||
|
originalMimeType: 'image/gif',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
duration: '2.0',
|
||||||
|
});
|
||||||
render(PhotoViewer, { asset });
|
render(PhotoViewer, { asset });
|
||||||
|
|
||||||
expect(getAssetThumbnailUrlSpy).not.toBeCalled();
|
expect(getAssetThumbnailUrlSpy).not.toBeCalled();
|
||||||
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
|
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('loads original for shared link when download permission is true and showMetadata permission is true', () => {
|
it('loads the original image for animated webp images', () => {
|
||||||
const asset = assetFactory.build({ originalPath: 'image.gif', originalMimeType: 'image/gif' });
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.webp',
|
||||||
|
originalMimeType: 'image/webp',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
duration: '2.0',
|
||||||
|
});
|
||||||
|
render(PhotoViewer, { asset });
|
||||||
|
|
||||||
|
expect(getAssetThumbnailUrlSpy).not.toBeCalled();
|
||||||
|
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('not loads original static image in shared link even when download permission is true and showMetadata permission is true', () => {
|
||||||
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.gif',
|
||||||
|
originalMimeType: 'image/gif',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
});
|
||||||
|
const sharedLink = sharedLinkFactory.build({ allowDownload: true, showMetadata: true, assets: [asset] });
|
||||||
|
render(PhotoViewer, { asset, sharedLink });
|
||||||
|
|
||||||
|
expect(getAssetThumbnailUrlSpy).toBeCalledWith({
|
||||||
|
id: asset.id,
|
||||||
|
size: AssetMediaSize.Preview,
|
||||||
|
cacheKey: asset.thumbhash,
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('loads original animated image in shared link when download permission is true and showMetadata permission is true', () => {
|
||||||
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.gif',
|
||||||
|
originalMimeType: 'image/gif',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
duration: '2.0',
|
||||||
|
});
|
||||||
const sharedLink = sharedLinkFactory.build({ allowDownload: true, showMetadata: true, assets: [asset] });
|
const sharedLink = sharedLinkFactory.build({ allowDownload: true, showMetadata: true, assets: [asset] });
|
||||||
render(PhotoViewer, { asset, sharedLink });
|
render(PhotoViewer, { asset, sharedLink });
|
||||||
|
|
||||||
|
|
@ -93,8 +170,13 @@ describe('PhotoViewer component', () => {
|
||||||
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
|
expect(getAssetOriginalUrlSpy).toBeCalledWith({ id: asset.id, cacheKey: asset.thumbhash });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not loads original image when shared link download permission is false', () => {
|
it('not loads original animated image when shared link download permission is false', () => {
|
||||||
const asset = assetFactory.build({ originalPath: 'image.gif', originalMimeType: 'image/gif' });
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.gif',
|
||||||
|
originalMimeType: 'image/gif',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
duration: '2.0',
|
||||||
|
});
|
||||||
const sharedLink = sharedLinkFactory.build({ allowDownload: false, assets: [asset] });
|
const sharedLink = sharedLinkFactory.build({ allowDownload: false, assets: [asset] });
|
||||||
render(PhotoViewer, { asset, sharedLink });
|
render(PhotoViewer, { asset, sharedLink });
|
||||||
|
|
||||||
|
|
@ -107,8 +189,13 @@ describe('PhotoViewer component', () => {
|
||||||
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
expect(getAssetOriginalUrlSpy).not.toBeCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('not loads original image when shared link showMetadata permission is false', () => {
|
it('not loads original animated image when shared link showMetadata permission is false', () => {
|
||||||
const asset = assetFactory.build({ originalPath: 'image.gif', originalMimeType: 'image/gif' });
|
const asset = assetFactory.build({
|
||||||
|
originalPath: 'image.gif',
|
||||||
|
originalMimeType: 'image/gif',
|
||||||
|
type: AssetTypeEnum.Image,
|
||||||
|
duration: '2.0',
|
||||||
|
});
|
||||||
const sharedLink = sharedLinkFactory.build({ showMetadata: false, assets: [asset] });
|
const sharedLink = sharedLinkFactory.build({ showMetadata: false, assets: [asset] });
|
||||||
render(PhotoViewer, { asset, sharedLink });
|
render(PhotoViewer, { asset, sharedLink });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@
|
||||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, AssetTypeEnum, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { LoadingSpinner, toastManager } from '@immich/ui';
|
import { LoadingSpinner, toastManager } from '@immich/ui';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, onMount } from 'svelte';
|
||||||
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||||
|
|
@ -139,7 +139,10 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
// when true, will force loading of the original image
|
// when true, will force loading of the original image
|
||||||
let forceUseOriginal: boolean = $derived(asset.originalMimeType === 'image/gif' || $photoZoomState.currentZoom > 1);
|
let forceUseOriginal: boolean = $derived(
|
||||||
|
(asset.type === AssetTypeEnum.Image && asset.duration && !asset.duration.includes('0:00:00.000')) ||
|
||||||
|
$photoZoomState.currentZoom > 1,
|
||||||
|
);
|
||||||
|
|
||||||
const targetImageSize = $derived.by(() => {
|
const targetImageSize = $derived.by(() => {
|
||||||
if ($alwaysLoadOriginalFile || forceUseOriginal || originalImageLoaded) {
|
if ($alwaysLoadOriginalFile || forceUseOriginal || originalImageLoaded) {
|
||||||
|
|
|
||||||
|
|
@ -282,7 +282,7 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if asset.isImage && asset.duration}
|
{#if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000')}
|
||||||
<div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
|
<div class="absolute end-0 top-0 flex place-items-center gap-1 text-xs font-medium text-white">
|
||||||
<span class="pe-2 pt-2">
|
<span class="pe-2 pt-2">
|
||||||
<Icon icon={mdiFileGifBox} size="24" />
|
<Icon icon={mdiFileGifBox} size="24" />
|
||||||
|
|
@ -351,7 +351,7 @@
|
||||||
playbackOnIconHover={!$playVideoThumbnailOnHover}
|
playbackOnIconHover={!$playVideoThumbnailOnHover}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{:else if asset.isImage && asset.duration && mouseOver}
|
{:else if asset.isImage && asset.duration && !asset.duration.includes('0:00:00.000') && mouseOver}
|
||||||
<!-- GIF -->
|
<!-- GIF -->
|
||||||
<div class="absolute top-0 h-full w-full pointer-events-none">
|
<div class="absolute top-0 h-full w-full pointer-events-none">
|
||||||
<div class="absolute h-full w-full bg-linear-to-b from-black/25 via-[transparent_25%]"></div>
|
<div class="absolute h-full w-full bg-linear-to-b from-black/25 via-[transparent_25%]"></div>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue