mirror of
https://github.com/samsonjs/immich.git
synced 2026-04-27 15:07:45 +00:00
feat: redesign asset-viewer previous/next and hide when nav not possible (#24903)
This commit is contained in:
parent
d59ee7d2ae
commit
80a5444bf4
14 changed files with 299 additions and 321 deletions
|
|
@ -1,5 +1,5 @@
|
||||||
import { faker } from '@faker-js/faker';
|
import { faker } from '@faker-js/faker';
|
||||||
import { test } from '@playwright/test';
|
import { expect, test } from '@playwright/test';
|
||||||
import {
|
import {
|
||||||
Changes,
|
Changes,
|
||||||
createDefaultTimelineConfig,
|
createDefaultTimelineConfig,
|
||||||
|
|
@ -58,6 +58,120 @@ test.describe('asset-viewer', () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test.describe('/photos/:id', () => {
|
test.describe('/photos/:id', () => {
|
||||||
|
test('Navigate to next asset via button', async ({ page }) => {
|
||||||
|
const asset = selectRandom(assets, rng);
|
||||||
|
const index = assets.indexOf(asset);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||||
|
|
||||||
|
await page.getByLabel('View next asset').click();
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + 1].id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navigate to previous asset via button', async ({ page }) => {
|
||||||
|
const asset = selectRandom(assets, rng);
|
||||||
|
const index = assets.indexOf(asset);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||||
|
|
||||||
|
await page.getByLabel('View previous asset').click();
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - 1].id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navigate to next asset via keyboard (ArrowRight)', async ({ page }) => {
|
||||||
|
const asset = selectRandom(assets, rng);
|
||||||
|
const index = assets.indexOf(asset);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowRight');
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index + 1]);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + 1].id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navigate to previous asset via keyboard (ArrowLeft)', async ({ page }) => {
|
||||||
|
const asset = selectRandom(assets, rng);
|
||||||
|
const index = assets.indexOf(asset);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||||
|
|
||||||
|
await page.keyboard.press('ArrowLeft');
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index - 1]);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - 1].id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navigate forward 5 times via button', async ({ page }) => {
|
||||||
|
const asset = selectRandom(assets, rng);
|
||||||
|
const index = assets.indexOf(asset);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||||
|
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
await page.getByLabel('View next asset').click();
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index + i]);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index + i].id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navigate backward 5 times via button', async ({ page }) => {
|
||||||
|
const asset = selectRandom(assets, rng);
|
||||||
|
const index = assets.indexOf(asset);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||||
|
|
||||||
|
for (let i = 1; i <= 5; i++) {
|
||||||
|
await page.getByLabel('View previous asset').click();
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index - i]);
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${assets[index - i].id}`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Navigate forward then backward via keyboard', async ({ page }) => {
|
||||||
|
const asset = selectRandom(assets, rng);
|
||||||
|
const index = assets.indexOf(asset);
|
||||||
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, asset);
|
||||||
|
|
||||||
|
// Navigate forward 3 times
|
||||||
|
for (let i = 1; i <= 3; i++) {
|
||||||
|
await page.keyboard.press('ArrowRight');
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index + i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Navigate backward 3 times to return to original
|
||||||
|
for (let i = 2; i >= 0; i--) {
|
||||||
|
await page.keyboard.press('ArrowLeft');
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, assets[index + i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Verify we're back at the original asset
|
||||||
|
await expect.poll(() => new URL(page.url()).pathname).toBe(`/photos/${asset.id}`);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Verify no next button on last asset', async ({ page }) => {
|
||||||
|
const lastAsset = assets.at(-1)!;
|
||||||
|
await page.goto(`/photos/${lastAsset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, lastAsset);
|
||||||
|
|
||||||
|
// Verify next button doesn't exist
|
||||||
|
await expect(page.getByLabel('View next asset')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Verify no previous button on first asset', async ({ page }) => {
|
||||||
|
const firstAsset = assets[0];
|
||||||
|
await page.goto(`/photos/${firstAsset.id}`);
|
||||||
|
await assetViewerUtils.waitForViewerLoad(page, firstAsset);
|
||||||
|
|
||||||
|
// Verify previous button doesn't exist
|
||||||
|
await expect(page.getByLabel('View previous asset')).toHaveCount(0);
|
||||||
|
});
|
||||||
|
|
||||||
test('Delete photo advances to next', async ({ page }) => {
|
test('Delete photo advances to next', async ({ page }) => {
|
||||||
const asset = selectRandom(assets, rng);
|
const asset = selectRandom(assets, rng);
|
||||||
await page.goto(`/photos/${asset.id}`);
|
await page.goto(`/photos/${asset.id}`);
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import { Page, expect, test } from '@playwright/test';
|
||||||
import { utils } from 'src/utils';
|
import { utils } from 'src/utils';
|
||||||
|
|
||||||
function imageLocator(page: Page) {
|
function imageLocator(page: Page) {
|
||||||
return page.getByAltText('Image taken on').locator('visible=true');
|
return page.getByAltText('Image taken').locator('visible=true');
|
||||||
}
|
}
|
||||||
test.describe('Photo Viewer', () => {
|
test.describe('Photo Viewer', () => {
|
||||||
let admin: LoginResponseDto;
|
let admin: LoginResponseDto;
|
||||||
|
|
|
||||||
|
|
@ -19,6 +19,7 @@
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { getAssetJobMessage, getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils';
|
import { getAssetJobMessage, getAssetUrl, getSharedLink, handlePromiseError } from '$lib/utils';
|
||||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||||
|
import { navigateToAsset } from '$lib/utils/asset-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
import { InvocationTracker } from '$lib/utils/invocationTracker';
|
||||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||||
|
|
@ -52,8 +53,6 @@
|
||||||
import SlideshowBar from './slideshow-bar.svelte';
|
import SlideshowBar from './slideshow-bar.svelte';
|
||||||
import VideoViewer from './video-wrapper-viewer.svelte';
|
import VideoViewer from './video-wrapper-viewer.svelte';
|
||||||
|
|
||||||
type HasAsset = boolean;
|
|
||||||
|
|
||||||
export type AssetCursor = {
|
export type AssetCursor = {
|
||||||
current: AssetResponseDto;
|
current: AssetResponseDto;
|
||||||
nextAsset?: AssetResponseDto;
|
nextAsset?: AssetResponseDto;
|
||||||
|
|
@ -72,9 +71,7 @@
|
||||||
onAction?: OnAction;
|
onAction?: OnAction;
|
||||||
onUndoDelete?: OnUndoDelete;
|
onUndoDelete?: OnUndoDelete;
|
||||||
onClose?: (asset: AssetResponseDto) => void;
|
onClose?: (asset: AssetResponseDto) => void;
|
||||||
onNext: () => Promise<HasAsset>;
|
onRandom?: () => Promise<{ id: string } | undefined>;
|
||||||
onPrevious: () => Promise<HasAsset>;
|
|
||||||
onRandom: () => Promise<{ id: string } | undefined>;
|
|
||||||
copyImage?: () => Promise<void>;
|
copyImage?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -90,8 +87,6 @@
|
||||||
onAction,
|
onAction,
|
||||||
onUndoDelete,
|
onUndoDelete,
|
||||||
onClose,
|
onClose,
|
||||||
onNext,
|
|
||||||
onPrevious,
|
|
||||||
onRandom,
|
onRandom,
|
||||||
copyImage = $bindable(),
|
copyImage = $bindable(),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
@ -108,6 +103,8 @@
|
||||||
const stackSelectedThumbnailSize = 65;
|
const stackSelectedThumbnailSize = 65;
|
||||||
|
|
||||||
const asset = $derived(cursor.current);
|
const asset = $derived(cursor.current);
|
||||||
|
const nextAsset = $derived(cursor.nextAsset);
|
||||||
|
const previousAsset = $derived(cursor.previousAsset);
|
||||||
let appearsInAlbums: AlbumResponseDto[] = $state([]);
|
let appearsInAlbums: AlbumResponseDto[] = $state([]);
|
||||||
let sharedLink = getSharedLink();
|
let sharedLink = getSharedLink();
|
||||||
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
||||||
|
|
@ -235,14 +232,15 @@
|
||||||
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
||||||
hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
|
hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
|
||||||
if (!hasNext) {
|
if (!hasNext) {
|
||||||
const asset = await onRandom();
|
const asset = await onRandom?.();
|
||||||
if (asset) {
|
if (asset) {
|
||||||
slideshowHistory.queue(asset);
|
slideshowHistory.queue(asset);
|
||||||
hasNext = true;
|
hasNext = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
hasNext = order === 'previous' ? await onPrevious() : await onNext();
|
hasNext =
|
||||||
|
order === 'previous' ? await navigateToAsset(cursor.previousAsset) : await navigateToAsset(cursor.nextAsset);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($slideshowState === SlideshowState.PlaySlideshow) {
|
if ($slideshowState === SlideshowState.PlaySlideshow) {
|
||||||
|
|
@ -383,7 +381,6 @@
|
||||||
await ocrManager.getAssetOcr(asset.id);
|
await ocrManager.getAssetOcr(asset.id);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
asset;
|
asset;
|
||||||
|
|
@ -406,6 +403,42 @@
|
||||||
cursor.current = update;
|
cursor.current = update;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const viewerKind = $derived.by(() => {
|
||||||
|
if (previewStackedAsset) {
|
||||||
|
return asset.type === AssetTypeEnum.Image ? 'StackPhotoViewer' : 'StackVideoViewer';
|
||||||
|
}
|
||||||
|
if (asset.type === AssetTypeEnum.Video) {
|
||||||
|
return 'VideoViewer';
|
||||||
|
}
|
||||||
|
if (assetViewerManager.isPlayingMotionPhoto && asset.livePhotoVideoId) {
|
||||||
|
return 'LiveVideoViewer';
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR ||
|
||||||
|
(asset.originalPath && asset.originalPath.toLowerCase().endsWith('.insp'))
|
||||||
|
) {
|
||||||
|
return 'ImagePanaramaViewer';
|
||||||
|
}
|
||||||
|
if (isShowEditor && editManager.selectedTool?.type === EditToolType.Transform) {
|
||||||
|
return 'CropArea';
|
||||||
|
}
|
||||||
|
return 'PhotoViewer';
|
||||||
|
});
|
||||||
|
|
||||||
|
const showActivityStatus = $derived(
|
||||||
|
$slideshowState === SlideshowState.None &&
|
||||||
|
isShared &&
|
||||||
|
((album && album.isActivityEnabled) || activityManager.commentCount > 0) &&
|
||||||
|
!activityManager.isLoading,
|
||||||
|
);
|
||||||
|
|
||||||
|
const showOcrButton = $derived(
|
||||||
|
$slideshowState === SlideshowState.None &&
|
||||||
|
asset.type === AssetTypeEnum.Image &&
|
||||||
|
!isShowEditor &&
|
||||||
|
ocrManager.hasOcrData,
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<OnEvents {onAssetReplace} {onAssetUpdate} />
|
<OnEvents {onAssetReplace} {onAssetUpdate} />
|
||||||
|
|
@ -442,7 +475,7 @@
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $slideshowState != SlideshowState.None}
|
{#if $slideshowState != SlideshowState.None}
|
||||||
<div class="absolute w-full flex">
|
<div class="absolute w-full flex justify-center">
|
||||||
<SlideshowBar
|
<SlideshowBar
|
||||||
{isFullScreen}
|
{isFullScreen}
|
||||||
assetType={previewStackedAsset?.type ?? asset.type}
|
assetType={previewStackedAsset?.type ?? asset.type}
|
||||||
|
|
@ -454,109 +487,97 @@
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor}
|
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor && previousAsset}
|
||||||
<div class="my-auto column-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
|
<div class="my-auto col-span-1 col-start-1 row-span-full row-start-1 justify-self-start">
|
||||||
<PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} />
|
<PreviousAssetAction onPreviousAsset={() => navigateAsset('previous')} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Asset Viewer -->
|
<!-- Asset Viewer -->
|
||||||
<div class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
|
<div class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
|
||||||
{#if previewStackedAsset}
|
{#if viewerKind === 'StackPhotoViewer'}
|
||||||
{#key previewStackedAsset.id}
|
<PhotoViewer
|
||||||
{#if previewStackedAsset.type === AssetTypeEnum.Image}
|
bind:zoomToggle
|
||||||
<PhotoViewer
|
bind:copyImage
|
||||||
bind:zoomToggle
|
cursor={{ ...cursor, current: previewStackedAsset! }}
|
||||||
bind:copyImage
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
cursor={{ ...cursor, current: previewStackedAsset }}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
haveFadeTransition={false}
|
||||||
onNextAsset={() => navigateAsset('next')}
|
{sharedLink}
|
||||||
haveFadeTransition={false}
|
/>
|
||||||
{sharedLink}
|
{:else if viewerKind === 'StackVideoViewer'}
|
||||||
/>
|
<VideoViewer
|
||||||
{:else}
|
assetId={previewStackedAsset!.id}
|
||||||
<VideoViewer
|
cacheKey={previewStackedAsset!.thumbhash}
|
||||||
assetId={previewStackedAsset.id}
|
projectionType={previewStackedAsset!.exifInfo?.projectionType}
|
||||||
cacheKey={previewStackedAsset.thumbhash}
|
loopVideo={true}
|
||||||
projectionType={previewStackedAsset.exifInfo?.projectionType}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
loopVideo={true}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onClose={closeViewer}
|
||||||
onNextAsset={() => navigateAsset('next')}
|
onVideoEnded={() => navigateAsset()}
|
||||||
onClose={closeViewer}
|
onVideoStarted={handleVideoStarted}
|
||||||
onVideoEnded={() => navigateAsset()}
|
{playOriginalVideo}
|
||||||
onVideoStarted={handleVideoStarted}
|
/>
|
||||||
{playOriginalVideo}
|
{:else if viewerKind === 'LiveVideoViewer'}
|
||||||
/>
|
<VideoViewer
|
||||||
{/if}
|
assetId={asset.livePhotoVideoId!}
|
||||||
{/key}
|
cacheKey={asset.thumbhash}
|
||||||
{:else}
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
{#key asset.id}
|
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||||
{#if asset.type === AssetTypeEnum.Image}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
{#if assetViewerManager.isPlayingMotionPhoto && asset.livePhotoVideoId}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
<VideoViewer
|
onVideoEnded={() => (assetViewerManager.isPlayingMotionPhoto = false)}
|
||||||
assetId={asset.livePhotoVideoId}
|
{playOriginalVideo}
|
||||||
cacheKey={asset.thumbhash}
|
/>
|
||||||
projectionType={asset.exifInfo?.projectionType}
|
{:else if viewerKind === 'ImagePanaramaViewer'}
|
||||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
<ImagePanoramaViewer bind:zoomToggle {asset} />
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
{:else if viewerKind === 'CropArea'}
|
||||||
onNextAsset={() => navigateAsset('next')}
|
<CropArea {asset} />
|
||||||
onVideoEnded={() => (assetViewerManager.isPlayingMotionPhoto = false)}
|
{:else if viewerKind === 'PhotoViewer'}
|
||||||
{playOriginalVideo}
|
<PhotoViewer
|
||||||
/>
|
bind:zoomToggle
|
||||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
bind:copyImage
|
||||||
.toLowerCase()
|
{cursor}
|
||||||
.endsWith('.insp'))}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
<ImagePanoramaViewer bind:zoomToggle {asset} />
|
onNextAsset={() => navigateAsset('next')}
|
||||||
{:else if isShowEditor && editManager.selectedTool?.type === EditToolType.Transform}
|
{sharedLink}
|
||||||
<CropArea {asset} />
|
haveFadeTransition={$slideshowState !== SlideshowState.None && $slideshowTransition}
|
||||||
{:else}
|
/>
|
||||||
<PhotoViewer
|
{:else if viewerKind === 'VideoViewer'}
|
||||||
bind:zoomToggle
|
<VideoViewer
|
||||||
bind:copyImage
|
assetId={asset.id}
|
||||||
{cursor}
|
cacheKey={asset.thumbhash}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
onNextAsset={() => navigateAsset('next')}
|
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||||
{sharedLink}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
haveFadeTransition={$slideshowState !== SlideshowState.None && $slideshowTransition}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
/>
|
onClose={closeViewer}
|
||||||
{/if}
|
onVideoEnded={() => navigateAsset()}
|
||||||
{:else}
|
onVideoStarted={handleVideoStarted}
|
||||||
<VideoViewer
|
{playOriginalVideo}
|
||||||
assetId={asset.id}
|
/>
|
||||||
cacheKey={asset.thumbhash}
|
{/if}
|
||||||
projectionType={asset.exifInfo?.projectionType}
|
|
||||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
|
||||||
onNextAsset={() => navigateAsset('next')}
|
|
||||||
onClose={closeViewer}
|
|
||||||
onVideoEnded={() => navigateAsset()}
|
|
||||||
onVideoStarted={handleVideoStarted}
|
|
||||||
{playOriginalVideo}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && isShared && ((album && album.isActivityEnabled) || activityManager.commentCount > 0) && !activityManager.isLoading}
|
{#if showActivityStatus}
|
||||||
<div class="absolute bottom-0 end-0 mb-20 me-8">
|
<div class="absolute bottom-0 end-0 mb-20 me-8">
|
||||||
<ActivityStatus
|
<ActivityStatus
|
||||||
disabled={!album?.isActivityEnabled}
|
disabled={!album?.isActivityEnabled}
|
||||||
isLiked={activityManager.isLiked}
|
isLiked={activityManager.isLiked}
|
||||||
numberOfComments={activityManager.commentCount}
|
numberOfComments={activityManager.commentCount}
|
||||||
numberOfLikes={activityManager.likeCount}
|
numberOfLikes={activityManager.likeCount}
|
||||||
onFavorite={handleFavorite}
|
onFavorite={handleFavorite}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && asset.type === AssetTypeEnum.Image && !isShowEditor && ocrManager.hasOcrData}
|
{#if showOcrButton}
|
||||||
<div class="absolute bottom-0 end-0 mb-6 me-6">
|
<div class="absolute bottom-0 end-0 mb-6 me-6">
|
||||||
<OcrButton />
|
<OcrButton />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor}
|
{#if $slideshowState === SlideshowState.None && showNavigation && !isShowEditor && nextAsset}
|
||||||
<div class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
|
<div class="my-auto col-span-1 col-start-4 row-span-full row-start-1 justify-self-end">
|
||||||
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
|
<NextAssetAction onNextAsset={() => navigateAsset('next')} />
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { AssetMediaSize, type SharedLinkResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { LoadingSpinner, toastManager } from '@immich/ui';
|
import { LoadingSpinner, toastManager } from '@immich/ui';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy, untrack } from 'svelte';
|
||||||
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
import { useSwipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
@ -164,11 +164,7 @@
|
||||||
imageError = imageLoaded = true;
|
imageError = imageLoaded = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onDestroy(() => preloadManager.cancelPreloadUrl(imageLoaderUrl));
|
||||||
return () => {
|
|
||||||
preloadManager.cancelPreloadUrl(imageLoaderUrl);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let imageLoaderUrl = $derived(
|
let imageLoaderUrl = $derived(
|
||||||
getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || $photoZoomState.currentZoom > 1 }),
|
getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || $photoZoomState.currentZoom > 1 }),
|
||||||
|
|
@ -181,9 +177,11 @@
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (lastUrl && lastUrl !== imageLoaderUrl) {
|
if (lastUrl && lastUrl !== imageLoaderUrl) {
|
||||||
imageLoaded = false;
|
untrack(() => {
|
||||||
originalImageLoaded = false;
|
imageLoaded = false;
|
||||||
imageError = false;
|
originalImageLoaded = false;
|
||||||
|
imageError = false;
|
||||||
|
});
|
||||||
}
|
}
|
||||||
lastUrl = imageLoaderUrl;
|
lastUrl = imageLoaderUrl;
|
||||||
});
|
});
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@
|
||||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
import { memoryStore, type MemoryAsset } from '$lib/stores/memory.store.svelte';
|
||||||
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
|
import { getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
|
||||||
|
|
@ -651,8 +651,6 @@
|
||||||
bind:this={memoryGallery}
|
bind:this={memoryGallery}
|
||||||
>
|
>
|
||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
onNext={handleNextAsset}
|
|
||||||
onPrevious={handlePreviousAsset}
|
|
||||||
assets={currentTimelineAssets}
|
assets={currentTimelineAssets}
|
||||||
viewport={galleryViewport}
|
viewport={galleryViewport}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
|
|
||||||
|
|
@ -144,13 +144,7 @@
|
||||||
{:else if assets.length === 1}
|
{:else if assets.length === 1}
|
||||||
{#await getAssetInfo({ ...authManager.params, id: assets[0].id }) then asset}
|
{#await getAssetInfo({ ...authManager.params, id: assets[0].id }) then asset}
|
||||||
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
<AssetViewer
|
<AssetViewer cursor={{ current: asset }} onAction={handleAction} />
|
||||||
cursor={{ current: asset }}
|
|
||||||
onAction={handleAction}
|
|
||||||
onPrevious={() => Promise.resolve(false)}
|
|
||||||
onNext={() => Promise.resolve(false)}
|
|
||||||
onRandom={() => Promise.resolve(undefined)}
|
|
||||||
/>
|
|
||||||
{/await}
|
{/await}
|
||||||
{/await}
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,13 @@
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { deleteAssets } from '$lib/utils/actions';
|
import { deleteAssets } from '$lib/utils/actions';
|
||||||
import { archiveAssets, cancelMultiselect, getNextAsset, getPreviousAsset } from '$lib/utils/asset-utils';
|
import {
|
||||||
|
archiveAssets,
|
||||||
|
cancelMultiselect,
|
||||||
|
getNextAsset,
|
||||||
|
getPreviousAsset,
|
||||||
|
navigateToAsset,
|
||||||
|
} from '$lib/utils/asset-utils';
|
||||||
import { moveFocus } from '$lib/utils/focus-util';
|
import { moveFocus } from '$lib/utils/focus-util';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
||||||
|
|
@ -26,7 +32,6 @@
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
initialAssetId?: string;
|
|
||||||
assets: AssetResponseDto[];
|
assets: AssetResponseDto[];
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
disableAssetSelect?: boolean;
|
disableAssetSelect?: boolean;
|
||||||
|
|
@ -34,9 +39,6 @@
|
||||||
viewport: Viewport;
|
viewport: Viewport;
|
||||||
onIntersected?: (() => void) | undefined;
|
onIntersected?: (() => void) | undefined;
|
||||||
showAssetName?: boolean;
|
showAssetName?: boolean;
|
||||||
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
|
|
||||||
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
|
|
||||||
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
|
|
||||||
onReload?: (() => void) | undefined;
|
onReload?: (() => void) | undefined;
|
||||||
pageHeaderOffset?: number;
|
pageHeaderOffset?: number;
|
||||||
slidingWindowOffset?: number;
|
slidingWindowOffset?: number;
|
||||||
|
|
@ -44,7 +46,6 @@
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let {
|
||||||
initialAssetId = undefined,
|
|
||||||
assets = $bindable(),
|
assets = $bindable(),
|
||||||
assetInteraction,
|
assetInteraction,
|
||||||
disableAssetSelect = false,
|
disableAssetSelect = false,
|
||||||
|
|
@ -52,16 +53,13 @@
|
||||||
viewport,
|
viewport,
|
||||||
onIntersected = undefined,
|
onIntersected = undefined,
|
||||||
showAssetName = false,
|
showAssetName = false,
|
||||||
onPrevious = undefined,
|
|
||||||
onNext = undefined,
|
|
||||||
onRandom = undefined,
|
|
||||||
onReload = undefined,
|
onReload = undefined,
|
||||||
slidingWindowOffset = 0,
|
slidingWindowOffset = 0,
|
||||||
pageHeaderOffset = 0,
|
pageHeaderOffset = 0,
|
||||||
arrowNavigation = true,
|
arrowNavigation = true,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
|
let { isViewing: isViewerOpen, asset: viewingAsset } = assetViewingStore;
|
||||||
|
|
||||||
const geometry = $derived(
|
const geometry = $derived(
|
||||||
getJustifiedLayoutFromAssets(assets, {
|
getJustifiedLayoutFromAssets(assets, {
|
||||||
|
|
@ -84,14 +82,6 @@
|
||||||
return top + pageHeaderOffset < window.bottom && top + geo.getHeight(i) > window.top;
|
return top + pageHeaderOffset < window.bottom && top + geo.getHeight(i) > window.top;
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentIndex = 0;
|
|
||||||
if (initialAssetId && assets.length > 0) {
|
|
||||||
const index = assets.findIndex(({ id }) => id === initialAssetId);
|
|
||||||
if (index !== -1) {
|
|
||||||
currentIndex = index;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
||||||
let scrollTop = $state(0);
|
let scrollTop = $state(0);
|
||||||
|
|
@ -105,7 +95,8 @@
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateCurrentAsset = (asset: AssetResponseDto) => {
|
const updateCurrentAsset = (asset: AssetResponseDto) => {
|
||||||
assets[currentIndex] = asset;
|
const index = assets.findIndex((oldAsset) => oldAsset.id === asset.id);
|
||||||
|
assets[index] = asset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const updateSlidingWindow = () => (scrollTop = document.scrollingElement?.scrollTop ?? 0);
|
const updateSlidingWindow = () => (scrollTop = document.scrollingElement?.scrollTop ?? 0);
|
||||||
|
|
@ -124,11 +115,6 @@
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const viewAssetHandler = async (asset: TimelineAsset) => {
|
|
||||||
currentIndex = assets.findIndex((a) => a.id == asset.id);
|
|
||||||
await setAssetId(assets[currentIndex].id);
|
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
};
|
|
||||||
|
|
||||||
const selectAllAssets = () => {
|
const selectAllAssets = () => {
|
||||||
assetInteraction.selectAssets(assets.map((a) => toTimelineAsset(a)));
|
assetInteraction.selectAssets(assets.map((a) => toTimelineAsset(a)));
|
||||||
|
|
@ -294,47 +280,13 @@
|
||||||
})(),
|
})(),
|
||||||
);
|
);
|
||||||
|
|
||||||
const handleNext = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
let asset: { id: string } | undefined;
|
|
||||||
if (onNext) {
|
|
||||||
asset = await onNext();
|
|
||||||
} else {
|
|
||||||
if (currentIndex >= assets.length - 1) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex = currentIndex + 1;
|
|
||||||
asset = currentIndex < assets.length ? assets[currentIndex] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!asset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await navigateToAsset(asset);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.cannot_navigate_next_asset'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRandom = async (): Promise<{ id: string } | undefined> => {
|
const handleRandom = async (): Promise<{ id: string } | undefined> => {
|
||||||
|
if (assets.length === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
let asset: { id: string } | undefined;
|
const randomIndex = Math.floor(Math.random() * assets.length);
|
||||||
if (onRandom) {
|
const asset = assets[randomIndex];
|
||||||
asset = await onRandom();
|
|
||||||
} else {
|
|
||||||
if (assets.length > 0) {
|
|
||||||
const randomIndex = Math.floor(Math.random() * assets.length);
|
|
||||||
asset = assets[randomIndex];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!asset) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await navigateToAsset(asset);
|
await navigateToAsset(asset);
|
||||||
return asset;
|
return asset;
|
||||||
|
|
@ -344,39 +296,6 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevious = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
let asset: { id: string } | undefined;
|
|
||||||
if (onPrevious) {
|
|
||||||
asset = await onPrevious();
|
|
||||||
} else {
|
|
||||||
if (currentIndex <= 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
currentIndex = currentIndex - 1;
|
|
||||||
asset = currentIndex >= 0 ? assets[currentIndex] : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!asset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await navigateToAsset(asset);
|
|
||||||
return true;
|
|
||||||
} catch (error) {
|
|
||||||
handleError(error, $t('errors.cannot_navigate_previous_asset'));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const navigateToAsset = async (asset?: { id: string }) => {
|
|
||||||
if (asset && asset.id !== $viewingAsset.id) {
|
|
||||||
await setAssetId(asset.id);
|
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleAction = async (action: Action) => {
|
const handleAction = async (action: Action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case AssetAction.ARCHIVE:
|
case AssetAction.ARCHIVE:
|
||||||
|
|
@ -387,11 +306,12 @@
|
||||||
1,
|
1,
|
||||||
);
|
);
|
||||||
if (assets.length === 0) {
|
if (assets.length === 0) {
|
||||||
await goto(AppRoute.PHOTOS);
|
return await goto(AppRoute.PHOTOS);
|
||||||
} else if (currentIndex === assets.length) {
|
}
|
||||||
await handlePrevious();
|
if (assetCursor.nextAsset) {
|
||||||
} else {
|
await navigateToAsset(assetCursor.nextAsset);
|
||||||
await setAssetId(assets[currentIndex].id);
|
} else if (assetCursor.previousAsset) {
|
||||||
|
await navigateToAsset(assetCursor.previousAsset);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
@ -454,7 +374,7 @@
|
||||||
handleSelectAssets(currentAsset);
|
handleSelectAssets(currentAsset);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void viewAssetHandler(currentAsset);
|
void navigateToAsset(asset);
|
||||||
}}
|
}}
|
||||||
onSelect={() => handleSelectAssets(currentAsset)}
|
onSelect={() => handleSelectAssets(currentAsset)}
|
||||||
onMouseEvent={() => assetMouseEventHandler(currentAsset)}
|
onMouseEvent={() => assetMouseEventHandler(currentAsset)}
|
||||||
|
|
@ -485,8 +405,6 @@
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
cursor={assetCursor}
|
cursor={assetCursor}
|
||||||
onAction={handleAction}
|
onAction={handleAction}
|
||||||
onPrevious={handlePrevious}
|
|
||||||
onNext={handleNext}
|
|
||||||
onRandom={handleRandom}
|
onRandom={handleRandom}
|
||||||
onAssetChange={updateCurrentAsset}
|
onAssetChange={updateCurrentAsset}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
import { updateStackedAssetInTimeline, updateUnstackedAssetInTimeline } from '$lib/utils/actions';
|
||||||
|
import { navigateToAsset } from '$lib/utils/asset-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk';
|
import { type AlbumResponseDto, type AssetResponseDto, type PersonResponseDto, getAssetInfo } from '@immich/sdk';
|
||||||
|
|
@ -24,7 +25,6 @@
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
album?: AlbumResponseDto;
|
album?: AlbumResponseDto;
|
||||||
person?: PersonResponseDto;
|
person?: PersonResponseDto;
|
||||||
|
|
||||||
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.SET_VISIBILITY_TIMELINE | null;
|
removeAction?: AssetAction.UNARCHIVE | AssetAction.ARCHIVE | AssetAction.SET_VISIBILITY_TIMELINE | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -41,7 +41,7 @@
|
||||||
const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => {
|
const getNextAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => {
|
||||||
const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset);
|
const earlierTimelineAsset = await timelineManager.getEarlierAsset(currentAsset);
|
||||||
if (earlierTimelineAsset) {
|
if (earlierTimelineAsset) {
|
||||||
const asset = await getAssetInfo({ ...authManager.params, id: earlierTimelineAsset.id });
|
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: earlierTimelineAsset.id });
|
||||||
if (preload) {
|
if (preload) {
|
||||||
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
|
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
|
||||||
void getNextAsset(asset, false);
|
void getNextAsset(asset, false);
|
||||||
|
|
@ -52,9 +52,8 @@
|
||||||
|
|
||||||
const getPreviousAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => {
|
const getPreviousAsset = async (currentAsset: AssetResponseDto, preload: boolean = true) => {
|
||||||
const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset);
|
const laterTimelineAsset = await timelineManager.getLaterAsset(currentAsset);
|
||||||
|
|
||||||
if (laterTimelineAsset) {
|
if (laterTimelineAsset) {
|
||||||
const asset = await getAssetInfo({ ...authManager.params, id: laterTimelineAsset.id });
|
const asset = await assetCacheManager.getAsset({ ...authManager.params, id: laterTimelineAsset.id });
|
||||||
if (preload) {
|
if (preload) {
|
||||||
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
|
// also pre-cache an extra one, to pre-cache these assetInfos for the next nav after this one is complete
|
||||||
void getPreviousAsset(asset, false);
|
void getPreviousAsset(asset, false);
|
||||||
|
|
@ -86,15 +85,6 @@
|
||||||
untrack(() => handlePromiseError(loadCloseAssets($viewingAsset)));
|
untrack(() => handlePromiseError(loadCloseAssets($viewingAsset)));
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleNavigateToAsset = async (targetAsset: AssetResponseDto | undefined | null) => {
|
|
||||||
if (!targetAsset) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
await navigate({ targetRoute: 'current', assetId: targetAsset.id });
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleRandom = async () => {
|
const handleRandom = async () => {
|
||||||
const randomAsset = await timelineManager.getRandomAsset();
|
const randomAsset = await timelineManager.getRandomAsset();
|
||||||
if (randomAsset) {
|
if (randomAsset) {
|
||||||
|
|
@ -124,8 +114,8 @@
|
||||||
|
|
||||||
// find the next asset to show or close the viewer
|
// find the next asset to show or close the viewer
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
// eslint-disable-next-line @typescript-eslint/no-unused-expressions
|
||||||
(await handleNavigateToAsset(assetCursor?.nextAsset)) ||
|
(await navigateToAsset(assetCursor?.nextAsset)) ||
|
||||||
(await handleNavigateToAsset(assetCursor?.previousAsset)) ||
|
(await navigateToAsset(assetCursor?.previousAsset)) ||
|
||||||
(await handleClose(action.asset));
|
(await handleClose(action.asset));
|
||||||
|
|
||||||
break;
|
break;
|
||||||
|
|
@ -197,18 +187,17 @@
|
||||||
await navigate({ targetRoute: 'current', assetId: restoredAsset.id });
|
await navigate({ targetRoute: 'current', assetId: restoredAsset.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
onDestroy(() => {
|
|
||||||
assetCacheManager.invalidate();
|
const handleUpdateOrUpload = (asset: AssetResponseDto) => {
|
||||||
});
|
|
||||||
const onAssetUpdate = ({ asset }: { event: 'upload' | 'update'; asset: AssetResponseDto }) => {
|
|
||||||
if (asset.id === assetCursor.current.id) {
|
if (asset.id === assetCursor.current.id) {
|
||||||
void loadCloseAssets(asset);
|
void loadCloseAssets(asset);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
websocketEvents.on('on_upload_success', (asset: AssetResponseDto) => onAssetUpdate({ event: 'upload', asset })),
|
websocketEvents.on('on_upload_success', (asset: AssetResponseDto) => handleUpdateOrUpload(asset)),
|
||||||
websocketEvents.on('on_asset_update', (asset: AssetResponseDto) => onAssetUpdate({ event: 'update', asset })),
|
websocketEvents.on('on_asset_update', (asset: AssetResponseDto) => handleUpdateOrUpload(asset)),
|
||||||
];
|
];
|
||||||
return () => {
|
return () => {
|
||||||
for (const unsubscribe of unsubscribes) {
|
for (const unsubscribe of unsubscribes) {
|
||||||
|
|
@ -216,6 +205,10 @@
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
onDestroy(() => {
|
||||||
|
assetCacheManager.invalidate();
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
|
|
@ -234,8 +227,6 @@
|
||||||
assetCacheManager.invalidate();
|
assetCacheManager.invalidate();
|
||||||
}}
|
}}
|
||||||
onUndoDelete={handleUndoDelete}
|
onUndoDelete={handleUndoDelete}
|
||||||
onPrevious={() => handleNavigateToAsset(assetCursor.previousAsset)}
|
|
||||||
onNext={() => handleNavigateToAsset(assetCursor.nextAsset)}
|
|
||||||
onRandom={handleRandom}
|
onRandom={handleRandom}
|
||||||
onClose={handleClose}
|
onClose={handleClose}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,6 @@
|
||||||
|
|
||||||
let { assets, onResolve, onStack }: Props = $props();
|
let { assets, onResolve, onStack }: Props = $props();
|
||||||
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
|
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
|
||||||
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
|
|
||||||
|
|
||||||
// eslint-disable-next-line svelte/no-unnecessary-state-wrap
|
// eslint-disable-next-line svelte/no-unnecessary-state-wrap
|
||||||
let selectedAssetIds = $state(new SvelteSet<string>());
|
let selectedAssetIds = $state(new SvelteSet<string>());
|
||||||
|
|
@ -44,24 +43,6 @@
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetViewingStore.showAssetViewer(false);
|
||||||
});
|
});
|
||||||
|
|
||||||
const onNext = async () => {
|
|
||||||
const index = getAssetIndex($viewingAsset.id) + 1;
|
|
||||||
if (index >= assets.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await onViewAsset(assets[index]);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPrevious = async () => {
|
|
||||||
const index = getAssetIndex($viewingAsset.id) - 1;
|
|
||||||
if (index < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await onViewAsset(assets[index]);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRandom = async () => {
|
const onRandom = async () => {
|
||||||
if (assets.length <= 0) {
|
if (assets.length <= 0) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -191,8 +172,6 @@
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
cursor={assetCursor}
|
cursor={assetCursor}
|
||||||
showNavigation={assets.length > 1}
|
showNavigation={assets.length > 1}
|
||||||
{onNext}
|
|
||||||
{onPrevious}
|
|
||||||
{onRandom}
|
{onRandom}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetViewingStore.showAssetViewer(false);
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@ import { readonly, writable } from 'svelte/store';
|
||||||
|
|
||||||
function createAssetViewingStore() {
|
function createAssetViewingStore() {
|
||||||
const viewingAssetStoreState = writable<AssetResponseDto>();
|
const viewingAssetStoreState = writable<AssetResponseDto>();
|
||||||
|
|
||||||
const viewState = writable<boolean>(false);
|
const viewState = writable<boolean>(false);
|
||||||
const gridScrollTarget = writable<AssetGridRouteSearchParams | null | undefined>();
|
const gridScrollTarget = writable<AssetGridRouteSearchParams | null | undefined>();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -508,11 +508,13 @@ export const delay = async (ms: number) => {
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getNextAsset = (assets: AssetResponseDto[], currentAsset: AssetResponseDto | undefined) => {
|
export const getNextAsset = (assets: AssetResponseDto[], currentAsset: AssetResponseDto | undefined) => {
|
||||||
return currentAsset && assets[assets.indexOf(currentAsset) + 1];
|
const index = currentAsset ? assets.findIndex((a) => a.id === currentAsset.id) : -1;
|
||||||
|
return index >= 0 ? assets[index + 1] : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getPreviousAsset = (assets: AssetResponseDto[], currentAsset: AssetResponseDto | undefined) => {
|
export const getPreviousAsset = (assets: AssetResponseDto[], currentAsset: AssetResponseDto | undefined) => {
|
||||||
return currentAsset && assets[assets.indexOf(currentAsset) - 1];
|
const index = currentAsset ? assets.findIndex((a) => a.id === currentAsset.id) : -1;
|
||||||
|
return index >= 0 ? assets[index - 1] : undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const canCopyImageToClipboard = (): boolean => {
|
export const canCopyImageToClipboard = (): boolean => {
|
||||||
|
|
@ -547,3 +549,12 @@ export const copyImageToClipboard = async (source: HTMLImageElement) => {
|
||||||
// do not await, so the Safari clipboard write happens in the context of the user gesture
|
// do not await, so the Safari clipboard write happens in the context of the user gesture
|
||||||
await navigator.clipboard.write([new ClipboardItem({ ['image/png']: imgToBlob(source) })]);
|
await navigator.clipboard.write([new ClipboardItem({ ['image/png']: imgToBlob(source) })]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const navigateToAsset = async (targetAsset: AssetResponseDto | undefined | null) => {
|
||||||
|
if (!targetAsset) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
await navigate({ targetRoute: 'current', assetId: targetAsset.id });
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
|
||||||
|
|
@ -103,7 +103,6 @@
|
||||||
{#if data.pathAssets && data.pathAssets.length > 0}
|
{#if data.pathAssets && data.pathAssets.length > 0}
|
||||||
<div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2">
|
<div bind:clientHeight={viewport.height} bind:clientWidth={viewport.width} class="mt-2">
|
||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
initialAssetId={data.asset?.id}
|
|
||||||
assets={data.pathAssets}
|
assets={data.pathAssets}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
{viewport}
|
{viewport}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
|
let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
|
||||||
|
|
||||||
let viewingAssets: string[] = $state([]);
|
let viewingAssets: string[] = $state([]);
|
||||||
let viewingAssetCursor = 0;
|
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetViewingStore.showAssetViewer(false);
|
||||||
|
|
@ -36,28 +35,9 @@
|
||||||
|
|
||||||
async function onViewAssets(assetIds: string[]) {
|
async function onViewAssets(assetIds: string[]) {
|
||||||
viewingAssets = assetIds;
|
viewingAssets = assetIds;
|
||||||
viewingAssetCursor = 0;
|
|
||||||
await setAssetId(assetIds[0]);
|
await setAssetId(assetIds[0]);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateNext() {
|
|
||||||
if (viewingAssetCursor < viewingAssets.length - 1) {
|
|
||||||
await setAssetId(viewingAssets[++viewingAssetCursor]);
|
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function navigatePrevious() {
|
|
||||||
if (viewingAssetCursor > 0) {
|
|
||||||
await setAssetId(viewingAssets[--viewingAssetCursor]);
|
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function navigateRandom() {
|
async function navigateRandom() {
|
||||||
if (viewingAssets.length <= 0) {
|
if (viewingAssets.length <= 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -138,13 +118,11 @@
|
||||||
</div>
|
</div>
|
||||||
</UserPageLayout>
|
</UserPageLayout>
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
{#if $showAssetViewer}
|
{#if $showAssetViewer && assetCursor.current}
|
||||||
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
cursor={assetCursor}
|
cursor={assetCursor}
|
||||||
showNavigation={viewingAssets.length > 1}
|
showNavigation={viewingAssets.length > 1}
|
||||||
onNext={navigateNext}
|
|
||||||
onPrevious={navigatePrevious}
|
|
||||||
onRandom={navigateRandom}
|
onRandom={navigateRandom}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetViewingStore.showAssetViewer(false);
|
||||||
|
|
|
||||||
|
|
@ -20,32 +20,12 @@
|
||||||
let assets = $derived(data.assets);
|
let assets = $derived(data.assets);
|
||||||
let asset = $derived(data.asset);
|
let asset = $derived(data.asset);
|
||||||
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
|
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
|
||||||
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
setAsset(asset);
|
setAsset(asset);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const onNext = async () => {
|
|
||||||
const index = getAssetIndex($viewingAsset.id) + 1;
|
|
||||||
if (index >= assets.length) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await onViewAsset(assets[index]);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPrevious = async () => {
|
|
||||||
const index = getAssetIndex($viewingAsset.id) - 1;
|
|
||||||
if (index < 0) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
await onViewAsset(assets[index]);
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const onRandom = async () => {
|
const onRandom = async () => {
|
||||||
if (assets.length <= 0) {
|
if (assets.length <= 0) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|
@ -94,8 +74,6 @@
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
cursor={assetCursor}
|
cursor={assetCursor}
|
||||||
showNavigation={assets.length > 1}
|
showNavigation={assets.length > 1}
|
||||||
{onNext}
|
|
||||||
{onPrevious}
|
|
||||||
{onRandom}
|
{onRandom}
|
||||||
{onAction}
|
{onAction}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue