mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor(web): asset-viewer code improvements
Change-Id: Ic8e750f4d6429a374c3a3f98542db8d36a6a6964
This commit is contained in:
@@ -108,6 +108,10 @@
|
|||||||
let sharedLink = getSharedLink();
|
let sharedLink = getSharedLink();
|
||||||
let fullscreenElement = $state<Element>();
|
let fullscreenElement = $state<Element>();
|
||||||
|
|
||||||
|
let slideShowPlaying = $derived($slideshowState === SlideshowState.PlaySlideshow);
|
||||||
|
let slideShowAscending = $derived($slideshowNavigation === SlideshowNavigation.AscendingOrder);
|
||||||
|
let slideShowShuffle = $derived($slideshowNavigation === SlideshowNavigation.Shuffle);
|
||||||
|
|
||||||
let playOriginalVideo = $state($alwaysLoadOriginalVideo);
|
let playOriginalVideo = $state($alwaysLoadOriginalVideo);
|
||||||
let slideshowStartAssetId = $state<string>();
|
let slideshowStartAssetId = $state<string>();
|
||||||
|
|
||||||
@@ -141,12 +145,6 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onAssetUpdate = (updatedAsset: AssetResponseDto) => {
|
|
||||||
if (asset.id === updatedAsset.id) {
|
|
||||||
cursor = { ...cursor, current: updatedAsset };
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let detailPanelTransitionName = $state<string | undefined>();
|
let detailPanelTransitionName = $state<string | undefined>();
|
||||||
let navigationBarTransitionName = $state<string | undefined>();
|
let navigationBarTransitionName = $state<string | undefined>();
|
||||||
let previousButtonTransitionName = $state<string | undefined>();
|
let previousButtonTransitionName = $state<string | undefined>();
|
||||||
@@ -230,68 +228,65 @@
|
|||||||
assetViewerManager.closeEditor();
|
assetViewerManager.closeEditor();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const completeNavigation = async (order: 'previous' | 'next') => {
|
||||||
|
preloadManager.cancelBeforeNavigation(order);
|
||||||
|
|
||||||
|
let hasNext: boolean;
|
||||||
|
|
||||||
|
if (slideShowPlaying && slideShowShuffle) {
|
||||||
|
let next = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
|
||||||
|
if (!next) {
|
||||||
|
const asset = await onRandom?.();
|
||||||
|
if (asset) {
|
||||||
|
slideshowHistory.queue(asset);
|
||||||
|
next = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
hasNext = next;
|
||||||
|
} else {
|
||||||
|
const target = order === 'previous' ? previousAsset : nextAsset;
|
||||||
|
hasNext = await navigateToAsset(target);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!slideShowPlaying) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasNext) {
|
||||||
|
$restartSlideshowProgress = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($slideshowRepeat && slideshowStartAssetId) {
|
||||||
|
await assetViewerManager.setAssetId(slideshowStartAssetId);
|
||||||
|
$restartSlideshowProgress = true;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await handleStopSlideshow();
|
||||||
|
};
|
||||||
|
|
||||||
const tracker = new InvocationTracker();
|
const tracker = new InvocationTracker();
|
||||||
let navigating = $state(false);
|
let navigating = $state(false);
|
||||||
const navigateAsset = (order?: 'previous' | 'next') => {
|
const navigateAsset = (order?: 'previous' | 'next') => {
|
||||||
if (!order) {
|
if (!order) {
|
||||||
if ($slideshowState === SlideshowState.PlaySlideshow) {
|
if (slideShowPlaying) {
|
||||||
order = $slideshowNavigation === SlideshowNavigation.AscendingOrder ? 'previous' : 'next';
|
order = slideShowAscending ? 'previous' : 'next';
|
||||||
} else {
|
} else {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
preloadManager.cancelBeforeNavigation(order);
|
|
||||||
|
|
||||||
if (tracker.isActive()) {
|
if (tracker.isActive()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
navigating = true;
|
navigating = true;
|
||||||
const navigation = tracker.invoke(async () => {
|
void tracker
|
||||||
const isShuffle =
|
.invoke(() => completeNavigation(order), $t('error_while_navigating'))
|
||||||
$slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle;
|
.finally(() => (navigating = false));
|
||||||
|
|
||||||
let hasNext: boolean;
|
|
||||||
|
|
||||||
if (isShuffle) {
|
|
||||||
hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
|
|
||||||
if (!hasNext) {
|
|
||||||
const asset = await onRandom?.();
|
|
||||||
if (asset) {
|
|
||||||
slideshowHistory.queue(asset);
|
|
||||||
hasNext = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
hasNext =
|
|
||||||
order === 'previous' ? await navigateToAsset(cursor.previousAsset) : await navigateToAsset(cursor.nextAsset);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($slideshowState !== SlideshowState.PlaySlideshow) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (hasNext) {
|
|
||||||
$restartSlideshowProgress = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if ($slideshowRepeat && slideshowStartAssetId) {
|
|
||||||
await assetViewerManager.setAssetId(slideshowStartAssetId);
|
|
||||||
$restartSlideshowProgress = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await handleStopSlideshow();
|
|
||||||
}, $t('error_while_navigating'));
|
|
||||||
void navigation.finally(() => (navigating = false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
|
||||||
* Slide show mode
|
|
||||||
*/
|
|
||||||
|
|
||||||
let assetViewerHtmlElement = $state<HTMLElement>();
|
let assetViewerHtmlElement = $state<HTMLElement>();
|
||||||
|
|
||||||
const slideshowHistory = new SlideshowHistory((asset) => {
|
const slideshowHistory = new SlideshowHistory((asset) => {
|
||||||
@@ -316,9 +311,11 @@
|
|||||||
|
|
||||||
const handleStopSlideshow = async () => {
|
const handleStopSlideshow = async () => {
|
||||||
try {
|
try {
|
||||||
if (document.fullscreenElement) {
|
if (!document.fullscreenElement) {
|
||||||
await document.exitFullscreen();
|
return;
|
||||||
}
|
}
|
||||||
|
document.body.style.cursor = '';
|
||||||
|
await document.exitFullscreen();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.unable_to_exit_fullscreen'));
|
handleError(error, $t('errors.unable_to_exit_fullscreen'));
|
||||||
} finally {
|
} finally {
|
||||||
@@ -421,14 +418,21 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (lastCursor) {
|
if (lastCursor) {
|
||||||
|
previewStackedAsset = undefined;
|
||||||
|
ocrManager.showOverlay = false;
|
||||||
preloadManager.updateAfterNavigation(lastCursor, cursor, sharedLink);
|
preloadManager.updateAfterNavigation(lastCursor, cursor, sharedLink);
|
||||||
}
|
} else {
|
||||||
if (!lastCursor) {
|
|
||||||
preloadManager.initializePreloads(cursor, sharedLink);
|
preloadManager.initializePreloads(cursor, sharedLink);
|
||||||
}
|
}
|
||||||
lastCursor = cursor;
|
lastCursor = cursor;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const onAssetUpdate = (update: AssetResponseDto) => {
|
||||||
|
if (asset.id === update.id) {
|
||||||
|
cursor = { ...cursor, current: update };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const viewerKind = $derived.by(() => {
|
const viewerKind = $derived.by(() => {
|
||||||
if (previewStackedAsset) {
|
if (previewStackedAsset) {
|
||||||
return previewStackedAsset.type === AssetTypeEnum.Image ? 'PhotoViewer' : 'StackVideoViewer';
|
return previewStackedAsset.type === AssetTypeEnum.Image ? 'PhotoViewer' : 'StackVideoViewer';
|
||||||
@@ -504,7 +508,6 @@
|
|||||||
use:focusTrap
|
use:focusTrap
|
||||||
bind:this={assetViewerHtmlElement}
|
bind:this={assetViewerHtmlElement}
|
||||||
>
|
>
|
||||||
<!-- Top navigation bar -->
|
|
||||||
{#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
|
{#if $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
|
||||||
<div
|
<div
|
||||||
class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform"
|
class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform"
|
||||||
@@ -551,11 +554,11 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Asset Viewer -->
|
|
||||||
<div data-viewer-content class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
|
<div data-viewer-content class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
|
||||||
{#if viewerKind === 'StackVideoViewer'}
|
{#if viewerKind === 'StackVideoViewer'}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
asset={previewStackedAsset!}
|
asset={previewStackedAsset!}
|
||||||
|
assetId={previewStackedAsset!.id}
|
||||||
cacheKey={previewStackedAsset!.thumbhash}
|
cacheKey={previewStackedAsset!.thumbhash}
|
||||||
projectionType={previewStackedAsset!.exifInfo?.projectionType}
|
projectionType={previewStackedAsset!.exifInfo?.projectionType}
|
||||||
loopVideo={true}
|
loopVideo={true}
|
||||||
@@ -647,7 +650,7 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if stack && withStacked && !assetViewerManager.isShowEditor}
|
{#if stack && withStacked && $slideshowState === SlideshowState.None && !assetViewerManager.isShowEditor}
|
||||||
{@const stackedAssets = stack.assets}
|
{@const stackedAssets = stack.assets}
|
||||||
<div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1 pointer-events-none">
|
<div id="stack-slideshow" class="absolute bottom-0 w-full col-span-4 col-start-1 pointer-events-none">
|
||||||
<div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar">
|
<div class="relative flex flex-row no-wrap overflow-x-auto overflow-y-hidden horizontal-scrollbar">
|
||||||
|
|||||||
@@ -35,6 +35,9 @@
|
|||||||
let { cursor, element = $bindable(), sharedLink, onError, onSwipe }: Props = $props();
|
let { cursor, element = $bindable(), sharedLink, onError, onSwipe }: Props = $props();
|
||||||
|
|
||||||
const { slideshowState, slideshowLook } = slideshowStore;
|
const { slideshowState, slideshowLook } = slideshowStore;
|
||||||
|
const objectFit = $derived(
|
||||||
|
$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.Cover ? 'cover' : 'contain',
|
||||||
|
);
|
||||||
const asset = $derived(cursor.current);
|
const asset = $derived(cursor.current);
|
||||||
|
|
||||||
let visibleImageReady: boolean = $state(false);
|
let visibleImageReady: boolean = $state(false);
|
||||||
@@ -226,7 +229,7 @@
|
|||||||
{asset}
|
{asset}
|
||||||
{sharedLink}
|
{sharedLink}
|
||||||
{container}
|
{container}
|
||||||
objectFit={$slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.Cover ? 'cover' : 'contain'}
|
{objectFit}
|
||||||
{onUrlChange}
|
{onUrlChange}
|
||||||
onImageReady={() => {
|
onImageReady={() => {
|
||||||
visibleImageReady = true;
|
visibleImageReady = true;
|
||||||
|
|||||||
@@ -58,7 +58,6 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
// reactive on `assetFileUrl` changes
|
|
||||||
if (assetFileUrl) {
|
if (assetFileUrl) {
|
||||||
hasFocused = false;
|
hasFocused = false;
|
||||||
videoPlayer?.load();
|
videoPlayer?.load();
|
||||||
|
|||||||
@@ -9,8 +9,8 @@
|
|||||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import SkipLink from '$lib/elements/SkipLink.svelte';
|
import SkipLink from '$lib/elements/SkipLink.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte';
|
|
||||||
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
import { featureFlagsManager } from '$lib/managers/feature-flags-manager.svelte';
|
||||||
|
import { viewTransitionManager } from '$lib/managers/ViewTransitionManager.svelte';
|
||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { getGlobalActions } from '$lib/services/app.service';
|
import { getGlobalActions } from '$lib/services/app.service';
|
||||||
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
|
import { mediaQueryManager } from '$lib/stores/media-query-manager.svelte';
|
||||||
@@ -37,16 +37,11 @@
|
|||||||
let shouldShowNotificationPanel = $state(false);
|
let shouldShowNotificationPanel = $state(false);
|
||||||
let innerWidth: number = $state(0);
|
let innerWidth: number = $state(0);
|
||||||
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
|
const hasUnreadNotifications = $derived(notificationManager.notifications.length > 0);
|
||||||
|
const { Cast } = $derived(getGlobalActions($t));
|
||||||
onMount(async () => {
|
|
||||||
try {
|
|
||||||
await notificationManager.refresh();
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Failed to load notifications on mount', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
|
void notificationManager.refresh().catch((error) => console.error('Failed to load notifications on mount', error));
|
||||||
|
|
||||||
return viewTransitionManager.on({
|
return viewTransitionManager.on({
|
||||||
PrepareOldSnapshot: (types) => {
|
PrepareOldSnapshot: (types) => {
|
||||||
if (types.includes('viewer')) {
|
if (types.includes('viewer')) {
|
||||||
@@ -61,8 +56,6 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const { Cast } = $derived(getGlobalActions($t));
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:window bind:innerWidth />
|
<svelte:window bind:innerWidth />
|
||||||
|
|||||||
Reference in New Issue
Block a user