refactor: zoom support

This commit is contained in:
wuzihao051119
2025-06-30 03:07:33 +08:00
parent 769d0aed87
commit b8dc1a4b1f
37 changed files with 369 additions and 354 deletions
-25
View File
@@ -1,25 +0,0 @@
import { photoZoomState } from '$lib/stores/zoom-image.store';
import { useZoomImageWheel } from '@zoom-image/svelte';
import { get } from 'svelte/store';
export const zoomImageAction = (node: HTMLElement) => {
const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel();
createZoomImage(node, {
maxZoom: 10,
});
const state = get(photoZoomState);
if (state) {
setZoomImageState(state);
}
const unsubscribes = [photoZoomState.subscribe(setZoomImageState), zoomImageState.subscribe(photoZoomState.set)];
return {
destroy() {
for (const unsubscribe of unsubscribes) {
unsubscribe();
}
},
};
};
@@ -1,5 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte';
import MapModal from '$lib/modals/MapModal.svelte'; import MapModal from '$lib/modals/MapModal.svelte';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
@@ -4,7 +4,7 @@
import AlbumMap from '$lib/components/album-page/album-map.svelte'; import AlbumMap from '$lib/components/album-page/album-map.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte'; import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte'; import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store'; import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
@@ -1,7 +1,7 @@
<script lang="ts"> <script lang="ts">
import { shortcut } from '$lib/actions/shortcut'; import { shortcut } from '$lib/actions/shortcut';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { downloadFile } from '$lib/utils/asset-utils'; import { downloadFile } from '$lib/utils/asset-utils';
import { IconButton } from '@immich/ui'; import { IconButton } from '@immich/ui';
import { mdiFolderDownloadOutline } from '@mdi/js'; import { mdiFolderDownloadOutline } from '@mdi/js';
@@ -21,9 +21,8 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte'; import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.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 } from '$lib/utils/asset-utils';
import { openFileUploadDialog } from '$lib/utils/file-uploader'; import { openFileUploadDialog } from '$lib/utils/file-uploader';
@@ -93,7 +92,8 @@
motionPhoto, motionPhoto,
}: Props = $props(); }: Props = $props();
let asset = $derived(assetManager.asset); let asset = $derived(assetManager.asset!);
let zoomImageState = $derived(assetManager.zoomImageState);
const sharedLink = getSharedLink(); const sharedLink = getSharedLink();
let isOwner = $derived($user && asset.ownerId === $user?.id); let isOwner = $derived($user && asset.ownerId === $user?.id);
@@ -143,7 +143,7 @@
color="secondary" color="secondary"
variant="ghost" variant="ghost"
shape="round" shape="round"
icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline} icon={zoomImageState && zoomImageState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
aria-label={$t('zoom_image')} aria-label={$t('zoom_image')}
onclick={onZoomImage} onclick={onZoomImage}
/> />
@@ -7,7 +7,7 @@
import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte'; import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte';
import { AssetAction, ProjectionType } from '$lib/constants'; import { AssetAction, ProjectionType } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte'; import { activityManager } from '$lib/managers/activity-manager.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { closeEditorCofirm } from '$lib/stores/asset-editor.store'; import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
import { isShowDetail } from '$lib/stores/preferences.store'; import { isShowDetail } from '$lib/stores/preferences.store';
@@ -96,7 +96,7 @@
let shouldPlayMotionPhoto = $state(false); let shouldPlayMotionPhoto = $state(false);
let sharedLink = getSharedLink(); let sharedLink = getSharedLink();
let enableDetailPanel = $derived(asset.hasMetadata); let enableDetailPanel = $derived(asset?.hasMetadata ?? false);
let slideshowStateUnsubscribe: () => void; let slideshowStateUnsubscribe: () => void;
let shuffleSlideshowUnsubscribe: () => void; let shuffleSlideshowUnsubscribe: () => void;
let previewStackedAsset: AssetResponseDto | undefined = $state(); let previewStackedAsset: AssetResponseDto | undefined = $state();
@@ -442,8 +442,7 @@
{#if asset.type === AssetTypeEnum.Image} {#if asset.type === AssetTypeEnum.Image}
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId} {#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
<VideoViewer <VideoViewer
assetId={asset.livePhotoVideoId} {assetManager}
cacheKey={asset.thumbhash}
projectionType={asset.exifInfo?.projectionType} projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow} loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
onPreviousAsset={() => navigateAsset('previous')} onPreviousAsset={() => navigateAsset('previous')}
@@ -453,15 +452,14 @@
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath {:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
.toLowerCase() .toLowerCase()
.endsWith('.insp'))} .endsWith('.insp'))}
<ImagePanoramaViewer {asset} /> <ImagePanoramaViewer {assetManager} />
{:else if isShowEditor && selectedEditType === 'crop'} {:else if isShowEditor && selectedEditType === 'crop'}
<CropArea {asset} /> <CropArea {asset} />
{:else} {:else}
<PhotoViewer <PhotoViewer
bind:zoomToggle bind:zoomToggle
bind:copyImage bind:copyImage
{asset} {assetManager}
{preloadAssets}
onPreviousAsset={() => navigateAsset('previous')} onPreviousAsset={() => navigateAsset('previous')}
onNextAsset={() => navigateAsset('next')} onNextAsset={() => navigateAsset('next')}
{sharedLink} {sharedLink}
@@ -470,8 +468,7 @@
{/if} {/if}
{:else} {:else}
<VideoViewer <VideoViewer
assetId={asset.id} {assetManager}
cacheKey={asset.thumbhash}
projectionType={asset.exifInfo?.projectionType} projectionType={asset.exifInfo?.projectionType}
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow} loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
onPreviousAsset={() => navigateAsset('previous')} onPreviousAsset={() => navigateAsset('previous')}
@@ -5,6 +5,7 @@
import { onDestroy, onMount, tick } from 'svelte'; import { onDestroy, onMount, tick } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { import {
changedOriention, changedOriention,
cropAspectRatio, cropAspectRatio,
@@ -13,7 +14,6 @@
rotateDegrees, rotateDegrees,
} from '$lib/stores/asset-editor.store'; } from '$lib/stores/asset-editor.store';
import { toTimelineAsset } from '$lib/utils/timeline-util'; import { toTimelineAsset } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk';
import { animateCropChange, recalculateCrop } from './crop-settings'; import { animateCropChange, recalculateCrop } from './crop-settings';
import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store'; import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store';
import { draw } from './drawing'; import { draw } from './drawing';
@@ -21,10 +21,10 @@
import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers'; import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers';
interface Props { interface Props {
asset: AssetResponseDto; assetManager: AssetManager;
} }
let { asset }: Props = $props(); let { assetManager = $bindable() }: Props = $props();
let img = $state<HTMLImageElement>(); let img = $state<HTMLImageElement>();
@@ -1,17 +1,19 @@
<script lang="ts"> <script lang="ts">
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { getAssetOriginalUrl } from '$lib/utils'; import { getAssetOriginalUrl } from '$lib/utils';
import { isWebCompatibleImage } from '$lib/utils/asset-utils'; import { isWebCompatibleImage } from '$lib/utils/asset-utils';
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk'; import { AssetMediaSize, viewAsset } from '@immich/sdk';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
interface Props { interface Props {
asset: AssetResponseDto; assetManager: AssetManager;
} }
const { asset }: Props = $props(); // TODO: do not preload assets.
const { assetManager = $bindable() }: Props = $props();
const loadAssetData = async (id: string) => { const loadAssetData = async (id: string) => {
const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: authManager.key }); const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: authManager.key });
@@ -1,26 +1,28 @@
<script lang="ts"> <script lang="ts">
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import { zoomImageAction } from '$lib/actions/zoom-image';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import BrokenAsset from '$lib/components/assets/broken-asset.svelte'; import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
import { assetViewerFadeDuration } from '$lib/constants'; import { assetViewerFadeDuration } from '$lib/constants';
import { type AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import {
cancelImageLoad,
mediaLoadError,
mediaLoaded,
} from '$lib/managers/asset-manager/internal/load-support.svelte';
import { zoomImageAttachment } from '$lib/managers/asset-manager/internal/zoom-support.svelte';
import { castManager } from '$lib/managers/cast-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { boundingBoxesArray } from '$lib/stores/people.store'; import { boundingBoxesArray } from '$lib/stores/people.store';
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store'; import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
import { photoZoomState } from '$lib/stores/zoom-image.store'; import { handlePromiseError } from '$lib/utils';
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils'; import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
import { getBoundingBox } from '$lib/utils/people-utils'; import { getBoundingBox } from '$lib/utils/people-utils';
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 { type SharedLinkResponseDto } from '@immich/sdk';
import { onDestroy, onMount } from 'svelte'; import { onDestroy } from 'svelte';
import { swipe, type SwipeCustomEvent } from 'svelte-gestures'; import { swipe, 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';
@@ -28,8 +30,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification'; import { NotificationType, notificationController } from '../shared-components/notification/notification';
interface Props { interface Props {
asset: AssetResponseDto; assetManager: AssetManager;
preloadAssets?: TimelineAsset[] | undefined;
element?: HTMLDivElement | undefined; element?: HTMLDivElement | undefined;
haveFadeTransition?: boolean; haveFadeTransition?: boolean;
sharedLink?: SharedLinkResponseDto | undefined; sharedLink?: SharedLinkResponseDto | undefined;
@@ -40,8 +41,7 @@
} }
let { let {
asset, assetManager = $bindable(),
preloadAssets = undefined,
element = $bindable(), element = $bindable(),
haveFadeTransition = true, haveFadeTransition = true,
sharedLink = undefined, sharedLink = undefined,
@@ -53,51 +53,25 @@
const { slideshowState, slideshowLook } = slideshowStore; const { slideshowState, slideshowLook } = slideshowStore;
let assetFileUrl: string = $state(''); let zoomImageState = $derived(assetManager.zoomImageState);
let imageLoaded: boolean = $state(false);
let originalImageLoaded: boolean = $state(false);
let imageError: boolean = $state(false);
let loader = $state<HTMLImageElement>(); zoomToggle = () => {
if (zoomImageState) {
photoZoomState.set({ zoomImageState.currentZoom = zoomImageState.currentZoom > 1 ? 1 : 2;
currentRotation: 0, }
currentZoom: 1, };
enable: true,
currentPositionX: 0,
currentPositionY: 0,
});
onDestroy(() => { onDestroy(() => {
$boundingBoxesArray = []; $boundingBoxesArray = [];
}); });
const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: TimelineAsset[]) => {
for (const preloadAsset of preloadAssets || []) {
if (preloadAsset.isImage) {
let img = new Image();
img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash);
}
}
};
const getAssetUrl = (id: string, targetSize: AssetMediaSize | 'original', cacheKey: string | null) => {
if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) {
return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
}
return targetSize === 'original'
? getAssetOriginalUrl({ id, cacheKey })
: getAssetThumbnailUrl({ id, size: targetSize, cacheKey });
};
copyImage = async () => { copyImage = async () => {
if (!canCopyImageToClipboard()) { if (!canCopyImageToClipboard()) {
return; return;
} }
try { try {
await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl); await copyImageToClipboard($photoViewerImgElement ?? assetManager.url!);
notificationController.show({ notificationController.show({
type: NotificationType.Info, type: NotificationType.Info,
message: $t('copied_image_to_clipboard'), message: $t('copied_image_to_clipboard'),
@@ -108,17 +82,10 @@
} }
}; };
zoomToggle = () => {
photoZoomState.set({
...$photoZoomState,
currentZoom: $photoZoomState.currentZoom > 1 ? 1 : 2,
});
};
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow); const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow);
$effect(() => { $effect(() => {
if (isFaceEditMode.value && $photoZoomState.currentZoom > 1) { if (isFaceEditMode.value && zoomImageState && zoomImageState.currentZoom > 1) {
zoomToggle(); zoomToggle();
} }
}); });
@@ -132,7 +99,7 @@
}; };
const onSwipe = (event: SwipeCustomEvent) => { const onSwipe = (event: SwipeCustomEvent) => {
if ($photoZoomState.currentZoom > 1) { if (!zoomImageState || zoomImageState.currentZoom > 1) {
return; return;
} }
if (onNextAsset && event.detail.direction === 'left') { if (onNextAsset && event.detail.direction === 'left') {
@@ -143,21 +110,10 @@
} }
}; };
// when true, will force loading of the original image
let forceUseOriginal: boolean = $derived(asset.originalMimeType === 'image/gif' || $photoZoomState.currentZoom > 1);
const targetImageSize = $derived.by(() => {
if ($alwaysLoadOriginalFile || forceUseOriginal || originalImageLoaded) {
return isWebCompatibleImage(asset) ? 'original' : AssetMediaSize.Fullsize;
}
return AssetMediaSize.Preview;
});
$effect(() => { $effect(() => {
if (assetFileUrl) { if (assetManager.url) {
// this can't be in an async context with $effect // this can't be in an async context with $effect
void cast(assetFileUrl); void cast(assetManager.url);
} }
}); });
@@ -175,35 +131,10 @@
} }
}; };
const onload = () => { onDestroy(() => {
imageLoaded = true; cancelImageLoad(assetManager);
assetFileUrl = imageLoaderUrl;
originalImageLoaded = targetImageSize === AssetMediaSize.Fullsize || targetImageSize === 'original';
};
const onerror = () => {
imageError = imageLoaded = true;
};
$effect(() => {
preload(targetImageSize, preloadAssets);
}); });
onMount(() => {
if (loader?.complete) {
onload();
}
loader?.addEventListener('load', onload, { passive: true });
loader?.addEventListener('error', onerror, { passive: true });
return () => {
loader?.removeEventListener('load', onload);
loader?.removeEventListener('error', onerror);
cancelImageUrl(imageLoaderUrl);
};
});
let imageLoaderUrl = $derived(getAssetUrl(asset.id, targetImageSize, asset.thumbhash));
let containerWidth = $state(0); let containerWidth = $state(0);
let containerHeight = $state(0); let containerHeight = $state(0);
</script> </script>
@@ -217,27 +148,32 @@
{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false }, { shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false },
]} ]}
/> />
{#if imageError} {#if assetManager.loadError}
<div class="h-full w-full"> <div class="h-full w-full">
<BrokenAsset class="text-xl h-full w-full" /> <BrokenAsset class="text-xl h-full w-full" />
</div> </div>
{/if} {/if}
<!-- svelte-ignore a11y_missing_attribute -->
<img bind:this={loader} style="display:none" src={imageLoaderUrl} aria-hidden="true" />
<div <div
bind:this={element} bind:this={element}
class="relative h-full select-none" class="relative h-full select-none"
bind:clientWidth={containerWidth} bind:clientWidth={containerWidth}
bind:clientHeight={containerHeight} bind:clientHeight={containerHeight}
> >
<img style="display:none" src={imageLoaderUrl} alt="" {onload} {onerror} /> <img
{#if !imageLoaded} style="display:none"
src={assetManager.url}
alt=""
aria-hidden="true"
onload={() => mediaLoaded(assetManager)}
onerror={() => mediaLoadError(assetManager)}
/>
{#if !assetManager.isLoaded}
<div id="spinner" class="flex h-full items-center justify-center"> <div id="spinner" class="flex h-full items-center justify-center">
<LoadingSpinner /> <LoadingSpinner />
</div> </div>
{:else if !imageError} {:else if !assetManager.loadError}
<div <div
use:zoomImageAction {@attach zoomImageAttachment(assetManager)}
use:swipe={() => ({})} use:swipe={() => ({})}
onswipe={onSwipe} onswipe={onSwipe}
class="h-full w-full" class="h-full w-full"
@@ -245,7 +181,7 @@
> >
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
<img <img
src={assetFileUrl} src={assetManager.url}
alt="" alt=""
class="-z-1 absolute top-0 start-0 object-cover h-full w-full blur-lg" class="-z-1 absolute top-0 start-0 object-cover h-full w-full blur-lg"
draggable="false" draggable="false"
@@ -253,15 +189,15 @@
{/if} {/if}
<img <img
bind:this={$photoViewerImgElement} bind:this={$photoViewerImgElement}
src={assetFileUrl} src={assetManager.url}
alt={$getAltText(toTimelineAsset(asset))} alt={$getAltText(toTimelineAsset(assetManager.asset!))}
class="h-full w-full {$slideshowState === SlideshowState.None class="h-full w-full {$slideshowState === SlideshowState.None
? 'object-contain' ? 'object-contain'
: slideshowLookCssMapping[$slideshowLook]}" : slideshowLookCssMapping[$slideshowLook]}"
draggable="false" draggable="false"
/> />
<!-- eslint-disable-next-line svelte/require-each-key --> <!-- eslint-disable-next-line svelte/require-each-key -->
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox} {#each getBoundingBox($boundingBoxesArray, zoomImageState!, $photoViewerImgElement) as boundingbox}
<div <div
class="absolute border-solid border-white border-[3px] rounded-lg" class="absolute border-solid border-white border-[3px] rounded-lg"
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;" style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
@@ -270,7 +206,12 @@
</div> </div>
{#if isFaceEditMode.value} {#if isFaceEditMode.value}
<FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} /> <FaceEditor
htmlElement={$photoViewerImgElement}
{containerWidth}
{containerHeight}
assetId={assetManager.asset!.id}
/>
{/if} {/if}
{/if} {/if}
</div> </div>
@@ -3,6 +3,7 @@
import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte'; import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { assetViewerFadeDuration } from '$lib/constants'; import { assetViewerFadeDuration } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { castManager } from '$lib/managers/cast-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { loopVideo as loopVideoPreference, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store'; import { loopVideo as loopVideoPreference, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
@@ -16,9 +17,8 @@
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
interface Props { interface Props {
assetId: string; assetManager: AssetManager;
loopVideo: boolean; loopVideo: boolean;
cacheKey: string | null;
onPreviousAsset?: () => void; onPreviousAsset?: () => void;
onNextAsset?: () => void; onNextAsset?: () => void;
onVideoEnded?: () => void; onVideoEnded?: () => void;
@@ -27,9 +27,8 @@
} }
let { let {
assetId, assetManager = $bindable(),
loopVideo, loopVideo,
cacheKey,
onPreviousAsset = () => {}, onPreviousAsset = () => {},
onNextAsset = () => {}, onNextAsset = () => {},
onVideoEnded = () => {}, onVideoEnded = () => {},
@@ -3,12 +3,13 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { fade } from 'svelte/transition'; import { fade } from 'svelte/transition';
import LoadingSpinner from '../shared-components/loading-spinner.svelte'; import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
interface Props { interface Props {
assetId: string; assetManager: AssetManager;
} }
const { assetId }: Props = $props(); const { assetManager = $bindable() }: Props = $props();
const modules = Promise.all([ const modules = Promise.all([
import('./photo-sphere-viewer-adapter.svelte').then((module) => module.default), import('./photo-sphere-viewer-adapter.svelte').then((module) => module.default),
@@ -1,12 +1,12 @@
<script lang="ts"> <script lang="ts">
import { ProjectionType } from '$lib/constants';
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte'; import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte'; import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte';
import { ProjectionType } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
interface Props { interface Props {
assetId: string; assetManager: AssetManager;
projectionType: string | null | undefined; projectionType: string | null | undefined;
cacheKey: string | null;
loopVideo: boolean; loopVideo: boolean;
onClose?: () => void; onClose?: () => void;
onPreviousAsset?: () => void; onPreviousAsset?: () => void;
@@ -15,10 +15,10 @@
onVideoStarted?: () => void; onVideoStarted?: () => void;
} }
// TODO: do not preload assets.
let { let {
assetId, assetManager = $bindable(),
projectionType, projectionType,
cacheKey,
loopVideo, loopVideo,
onPreviousAsset, onPreviousAsset,
onClose, onClose,
@@ -29,12 +29,11 @@
</script> </script>
{#if projectionType === ProjectionType.EQUIRECTANGULAR} {#if projectionType === ProjectionType.EQUIRECTANGULAR}
<VideoPanoramaViewer {assetId} /> <VideoPanoramaViewer {assetManager} />
{:else} {:else}
<VideoNativeViewer <VideoNativeViewer
{loopVideo} {loopVideo}
{cacheKey} {assetManager}
{assetId}
{onPreviousAsset} {onPreviousAsset}
{onNextAsset} {onNextAsset}
{onVideoEnded} {onVideoEnded}
@@ -26,7 +26,7 @@
NotificationType, NotificationType,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
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';
@@ -12,7 +12,7 @@
import ChangeDate from '$lib/components/shared-components/change-date.svelte'; import ChangeDate from '$lib/components/shared-components/change-date.svelte';
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte'; import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte'; import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
@@ -439,35 +439,38 @@
}; };
const handlePrevious = async () => { const handlePrevious = async () => {
const laterAsset = await timelineManager.getLaterAsset(asset); let laterAsset = undefined;
if (asset) {
if (laterAsset) { laterAsset = await timelineManager.getLaterAsset(asset);
// TODO: If preloadAsset is undefined, throw an exception. if (laterAsset) {
// assetManager.preloadAssets = [await timelineManager.getLaterAsset(laterAsset)]; // TODO: If preloadAsset is undefined, throw an exception.
await navigate({ targetRoute: 'current', assetId: laterAsset.id }); // assetManager.preloadAssets = [await timelineManager.getLaterAsset(laterAsset)];
await navigate({ targetRoute: 'current', assetId: laterAsset.id });
}
} }
return !!laterAsset; return !!laterAsset;
}; };
const handleNext = async () => { const handleNext = async () => {
const earlierAsset = await timelineManager.getEarlierAsset(asset); let earlierAsset = undefined;
if (asset) {
if (earlierAsset) { earlierAsset = await timelineManager.getEarlierAsset(asset);
// assetManager.preloadAssets = [await timelineManager.getEarlierAsset(earlierAsset)]; if (earlierAsset) {
await navigate({ targetRoute: 'current', assetId: earlierAsset.id }); // assetManager.preloadAssets = [await timelineManager.getEarlierAsset(earlierAsset)];
await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
}
} }
return !!earlierAsset; return !!earlierAsset;
}; };
const handleRandom = async () => { const handleRandom = async () => {
const randomAsset = await timelineManager.getRandomAsset(); let randomAsset = undefined;
if (asset) {
if (randomAsset) { randomAsset = await timelineManager.getRandomAsset();
await navigate({ targetRoute: 'current', assetId: randomAsset.id }); if (randomAsset) {
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
}
} }
return !!randomAsset; return !!randomAsset;
}; };
@@ -777,7 +780,7 @@
}); });
$effect(() => { $effect(() => {
if (assetManager.showAssetViewer) { if (assetManager.showAssetViewer && asset) {
const { localDateTime } = getTimes(asset.fileCreatedAt, DateTime.local().offset / 60); const { localDateTime } = getTimes(asset.fileCreatedAt, DateTime.local().offset / 60);
void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month }); void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month });
} }
@@ -3,7 +3,7 @@
import type { Action } from '$lib/components/asset-viewer/actions/action'; import type { Action } from '$lib/components/asset-viewer/actions/action';
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import type { Viewport } from '$lib/managers/timeline-manager/types'; import type { Viewport } from '$lib/managers/timeline-manager/types';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
@@ -4,7 +4,7 @@
import type { Action } from '$lib/components/asset-viewer/actions/action'; import type { Action } from '$lib/components/asset-viewer/actions/action';
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte';
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
@@ -2,7 +2,7 @@
import { shortcuts } from '$lib/actions/shortcut'; import { shortcuts } from '$lib/actions/shortcut';
import Portal from '$lib/components/shared-components/portal/portal.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte';
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte'; import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
import type { AssetManager } from '$lib/managers/asset-manager.svelte'; import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { suggestDuplicate } from '$lib/utils/duplicate-utils'; import { suggestDuplicate } from '$lib/utils/duplicate-utils';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
@@ -1,143 +0,0 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { CancellableTask } from '$lib/utils/cancellable-task';
import type { AssetGridRouteSearchParams } from '$lib/utils/navigation';
import {
AssetMediaSize,
AssetTypeEnum,
AssetVisibility,
getAllAlbums,
getAssetInfo,
getAssetOriginalPath,
getAssetPlaybackPath,
getAssetThumbnailPath,
getBaseUrl,
type AlbumResponseDto,
type AssetResponseDto,
} from '@immich/sdk';
import { isEqual } from 'lodash-es';
export type AssetManagerOptions = {
assetId?: string;
preloadAssetIds?: string[];
size?: AssetMediaSize;
};
export class AssetManager {
isInitialized = $state(false);
#asset: AssetResponseDto | undefined = $state();
preloadAssets: TimelineAsset[] = $state([]);
cacheKey: string | null = $state(null);
albums: AlbumResponseDto[] = $state([]);
showAssetViewer: boolean = $state(false);
gridScrollTarget: AssetGridRouteSearchParams | undefined = $state();
initTask = new CancellableTask(
() => (this.isInitialized = true),
() => (this.isInitialized = false),
() => void 0,
);
// TODO: Delete this after development
#emptyAsset: AssetResponseDto = {
checksum: '',
deviceAssetId: '',
deviceId: '',
duration: '',
fileCreatedAt: '',
fileModifiedAt: '',
hasMetadata: true,
id: '',
isArchived: false,
isFavorite: false,
isOffline: false,
isTrashed: false,
localDateTime: '',
originalFileName: '',
originalPath: '',
ownerId: '',
thumbhash: null,
type: AssetTypeEnum.Image,
updatedAt: '',
visibility: AssetVisibility.Timeline,
};
static #INIT_OPTIONS = {};
#options: AssetManagerOptions = AssetManager.#INIT_OPTIONS;
constructor() {}
async #initializeAsset(id: string) {
const assetResponse = await getAssetInfo({ id, key: authManager.key });
if (!assetResponse) {
return;
}
this.#asset = assetResponse;
const albumsResponse = await getAllAlbums({ assetId: this.asset.id });
if (!albumsResponse) {
return;
}
this.albums = albumsResponse;
}
async refreshAlbums() {}
async refreshAsset() {}
async updateOptions(options: AssetManagerOptions) {
if (this.#options !== AssetManager.#INIT_OPTIONS && isEqual(this.#options, options)) {
return;
}
await this.initTask.reset();
await this.#init(options);
}
async #init(options: AssetManagerOptions) {
this.isInitialized = false;
await this.initTask.execute(async () => {
this.#options = options;
// TODO: If assetId is undefined, throw an exception.
await this.#initializeAsset(this.#options.assetId!);
// TODO: Preload assets.
}, true);
}
public destroy() {
this.isInitialized = false;
}
#createUrl(path: string, parameters?: Record<string, unknown>) {
const searchParameters = new URLSearchParams();
for (const key in parameters) {
const value = parameters[key];
if (value !== undefined && value !== null) {
searchParameters.set(key, value.toString());
}
}
return getBaseUrl() + path + searchParameters.toString();
}
get asset() {
return this.#asset ?? this.#emptyAsset;
}
get originalUrl() {
return this.#createUrl(getAssetOriginalPath(this.asset.id), { key: authManager.key, c: this.cacheKey });
}
get thumbnailUrl() {
return this.#createUrl(getAssetThumbnailPath(this.asset.id), {
key: authManager.key,
c: this.cacheKey,
size: this.#options.size,
});
}
get playbackUrl() {
return this.#createUrl(getAssetPlaybackPath(this.asset.id), { key: authManager.key, c: this.cacheKey });
}
}
@@ -0,0 +1,204 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { CancellableTask } from '$lib/utils/cancellable-task';
import type { AssetGridRouteSearchParams } from '$lib/utils/navigation';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import {
getAllAlbums,
getAssetInfo,
getAssetOriginalPath,
getAssetPlaybackPath,
getAssetThumbnailPath,
getBaseUrl,
type AlbumResponseDto,
type AssetResponseDto,
} from '@immich/sdk';
import { type ZoomImageWheelState } from '@zoom-image/core';
import { isEqual } from 'lodash-es';
export enum AssetMediaSize {
Original = 'original',
Fullsize = 'fullsize',
Preview = 'preview',
Thumbnail = 'thumbnail',
Playback = 'playback',
}
export type AssetManagerOptions = {
assetId?: string;
preloadAssetIds?: string[];
loadAlbums?: boolean;
size?: AssetMediaSize;
};
export class AssetManager {
isInitialized = $state(false);
isLoaded = $state(false);
loadError = $state(false);
asset: AssetResponseDto | undefined = $state();
preloadAssets: TimelineAsset[] = $state([]);
albums: AlbumResponseDto[] = $state([]);
cacheKey: string | null = $derived(this.asset?.thumbhash ?? null);
url: string | undefined = $derived.by(() => {
if (this.asset) {
return this.#getAssetUrl(toTimelineAsset(this.asset!));
}
});
showAssetViewer: boolean = $state(false);
gridScrollTarget: AssetGridRouteSearchParams | undefined = $state();
zoomImageState: ZoomImageWheelState | undefined = $state();
initTask = new CancellableTask(
() => (this.isInitialized = true),
() => {
this.asset = undefined;
this.preloadAssets = [];
this.albums = [];
this.isInitialized = false;
},
() => void 0,
);
static #INIT_OPTIONS = {};
#options: AssetManagerOptions = AssetManager.#INIT_OPTIONS;
constructor() {}
async #initializeAsset() {
if (this.#options.assetId) {
const assetResponse = await getAssetInfo({ id: this.#options.assetId, key: authManager.key });
if (!assetResponse) {
return;
}
this.asset = assetResponse;
} else {
throw new Error('The assetId in required in options.');
}
// TODO: Preload assets.
if (this.#options.loadAlbums ?? true) {
const albumsResponse = await getAllAlbums({ assetId: this.#options.assetId });
if (!albumsResponse) {
return;
}
this.albums = albumsResponse;
}
}
async updateOptions(options: AssetManagerOptions) {
if (this.#options !== AssetManager.#INIT_OPTIONS && isEqual(this.#options, options)) {
return;
}
await this.initTask.reset();
await this.#init(options);
}
#checkOptions() {
this.#options.size = AssetMediaSize.Original;
if (!this.asset || !this.zoomImageState) {
return;
}
if (this.asset.originalMimeType === 'image/gif' || this.zoomImageState.currentZoom > 1) {
// TODO: use original image forcely and according to the setting.
}
}
async #init(options: AssetManagerOptions) {
this.isInitialized = false;
this.asset = undefined;
this.preloadAssets = [];
this.albums = [];
await this.initTask.execute(async () => {
this.#options = options;
await this.#initializeAsset();
this.#checkOptions();
}, true);
}
public destroy() {
this.isInitialized = false;
}
async refreshAlbums() {}
async refreshAsset() {}
#preload() {
for (const preloadAsset of this.preloadAssets) {
if (preloadAsset.isImage) {
let img = new Image();
const preloadUrl = this.#getAssetUrl(preloadAsset);
if (preloadUrl) {
img.src = preloadUrl;
} else {
throw new Error('AssetManager is not initialized.');
}
}
}
}
#getAssetUrl(asset: TimelineAsset) {
if (!this.asset) {
return;
}
let path = undefined;
const searchParameters = new URLSearchParams();
if (authManager.key) {
searchParameters.set('key', authManager.key);
}
if (this.cacheKey) {
searchParameters.set('c', this.cacheKey);
}
switch (this.#options.size) {
case AssetMediaSize.Original: {
path = getAssetOriginalPath(this.asset.id);
break;
}
case AssetMediaSize.Fullsize:
case AssetMediaSize.Thumbnail:
case AssetMediaSize.Preview: {
path = getAssetThumbnailPath(this.asset.id);
break;
}
case AssetMediaSize.Playback: {
path = getAssetPlaybackPath(this.asset.id);
break;
}
default:
// TODO: default AssetMediaSize
}
return getBaseUrl() + path + '?' + searchParameters.toString();
}
get isOriginalImage() {
return this.#options.size === AssetMediaSize.Original || this.#options.size === AssetMediaSize.Fullsize;
}
}
// const getAssetUrl = (id: string, targetSize: AssetMediaSize | 'original', cacheKey: string | null) => {
// if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) {
// return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
// }
// return targetSize === 'original'
// ? getAssetOriginalUrl({ id, cacheKey })
// : getAssetThumbnailUrl({ id, size: targetSize, cacheKey });
// };
// $effect(() => {
// if ($alwaysLoadOriginalFile || forceUseOriginal || originalImageLoaded) {
// assetManager.updateOptions({
// size: isWebCompatibleImage(asset) ? AssetMediaSize.Original : AssetMediaSize.Fullsize,
// });
// }
// assetManager.updateOptions({ size: AssetMediaSize.Preview });
// });
@@ -0,0 +1,17 @@
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { cancelImageUrl } from '$lib/utils/sw-messaging';
export function mediaLoaded(assetManager: AssetManager) {
assetManager.isLoaded = true;
}
export function mediaLoadError(assetManager: AssetManager) {
assetManager.isLoaded = assetManager.loadError = true;
}
export function cancelImageLoad(assetManager: AssetManager) {
if (assetManager.url) {
cancelImageUrl(assetManager.url);
}
assetManager.isLoaded = assetManager.loadError = false;
}
@@ -0,0 +1,24 @@
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { useZoomImageWheel } from '@zoom-image/svelte';
import type { Attachment } from 'svelte/attachments';
export function zoomImageAttachment(assetManager: AssetManager): Attachment<HTMLElement> {
return (element) => {
let zoomImage = $derived(assetManager.zoomImageState);
const { createZoomImage, zoomImageState, setZoomImageState } = useZoomImageWheel();
createZoomImage(element, { maxZoom: 10 });
$effect(() => {
if (zoomImage) {
setZoomImageState(zoomImage);
}
});
const unsubscribe = zoomImageState.subscribe((value) => (zoomImage = value));
return () => {
unsubscribe();
};
};
}
-4
View File
@@ -1,4 +0,0 @@
import type { ZoomImageWheelState } from '@zoom-image/core';
import { writable } from 'svelte/store';
export const photoZoomState = writable<ZoomImageWheelState>();
@@ -34,7 +34,7 @@
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AlbumPageViewMode, AppRoute } from '$lib/constants'; import { AlbumPageViewMode, AppRoute } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte'; import { activityManager } from '$lib/managers/activity-manager.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
@@ -14,7 +14,7 @@
import { AssetAction } from '$lib/constants'; import { AssetAction } from '$lib/constants';
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte'; import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
@@ -17,7 +17,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants'; import { AssetAction } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { preferences } from '$lib/stores/user.store'; import { preferences } from '$lib/stores/user.store';
@@ -32,7 +32,7 @@
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
interface Props { interface Props {
@@ -12,7 +12,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AppRoute, AssetAction } from '$lib/constants'; import { AppRoute, AssetAction } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility, lockAuthSession } from '@immich/sdk'; import { AssetVisibility, lockAuthSession } from '@immich/sdk';
@@ -4,7 +4,7 @@
import Map from '$lib/components/shared-components/map/map.svelte'; import Map from '$lib/components/shared-components/map/map.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { featureFlags } from '$lib/stores/server-config.store'; import { featureFlags } from '$lib/stores/server-config.store';
import { handlePromiseError } from '$lib/utils'; import { handlePromiseError } from '$lib/utils';
import { navigate } from '$lib/utils/navigation'; import { navigate } from '$lib/utils/navigation';
@@ -1,6 +1,6 @@
<script lang="ts"> <script lang="ts">
import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte'; import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import type { PageData } from './$types'; import type { PageData } from './$types';
@@ -8,7 +8,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte'; import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk'; import { AssetVisibility } from '@immich/sdk';
@@ -31,7 +31,7 @@
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants'; import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
@@ -22,7 +22,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants'; import { AssetAction } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
@@ -23,7 +23,7 @@
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
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 { AppRoute, QueryParameter } from '$lib/constants'; import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
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';
@@ -5,7 +5,7 @@
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
import PasswordField from '$lib/components/shared-components/password-field.svelte'; import PasswordField from '$lib/components/shared-components/password-field.svelte';
import ThemeButton from '$lib/components/shared-components/theme-button.svelte'; import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { user } from '$lib/stores/user.store'; import { user } from '$lib/stores/user.store';
import { setSharedLink } from '$lib/utils'; import { setSharedLink } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error'; import { handleError } from '$lib/utils/handle-error';
@@ -20,7 +20,7 @@
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
interface Props { interface Props {
data: PageData; data: PageData;
@@ -13,7 +13,7 @@
notificationController, notificationController,
} from '$lib/components/shared-components/notification/notification'; } from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants'; import { AppRoute } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte'; import { modalManager } from '$lib/managers/modal-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
@@ -20,7 +20,7 @@
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import type { PageData } from './$types'; import type { PageData } from './$types';
import { onDestroy } from 'svelte'; import { onDestroy } from 'svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte'; import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
interface Props { interface Props {
data: PageData; data: PageData;