From b8dc1a4b1fd19f5295fec3d565ea7f61b8560dde Mon Sep 17 00:00:00 2001 From: wuzihao051119 Date: Mon, 30 Jun 2025 03:07:33 +0800 Subject: [PATCH] refactor: zoom support --- web/src/lib/actions/zoom-image.ts | 25 --- .../components/album-page/album-map.svelte | 2 +- .../components/album-page/album-viewer.svelte | 2 +- .../actions/download-action.svelte | 2 +- .../asset-viewer/asset-viewer-nav-bar.svelte | 8 +- .../asset-viewer/asset-viewer.svelte | 15 +- .../editor/crop-tool/crop-area.svelte | 6 +- .../asset-viewer/image-panorama-viewer.svelte | 8 +- .../asset-viewer/photo-viewer.svelte | 155 +++++-------- .../asset-viewer/video-native-viewer.svelte | 7 +- .../asset-viewer/video-panorama-viewer.svelte | 5 +- .../asset-viewer/video-wrapper-viewer.svelte | 15 +- .../memory-page/memory-viewer.svelte | 2 +- .../components/photos-page/asset-grid.svelte | 43 ++-- .../individual-shared-viewer.svelte | 2 +- .../gallery-viewer/gallery-viewer.svelte | 2 +- .../duplicates-compare-control.svelte | 2 +- web/src/lib/managers/asset-manager.svelte.ts | 143 ------------ .../asset-manager/asset-manager.svelte.ts | 204 ++++++++++++++++++ .../internal/load-support.svelte.ts | 17 ++ .../internal/zoom-support.svelte.ts | 24 +++ web/src/lib/stores/zoom-image.store.ts | 4 - .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../(user)/photos/[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- .../[[assetId=id]]/+page.svelte | 2 +- 37 files changed, 369 insertions(+), 354 deletions(-) delete mode 100644 web/src/lib/actions/zoom-image.ts delete mode 100644 web/src/lib/managers/asset-manager.svelte.ts create mode 100644 web/src/lib/managers/asset-manager/asset-manager.svelte.ts create mode 100644 web/src/lib/managers/asset-manager/internal/load-support.svelte.ts create mode 100644 web/src/lib/managers/asset-manager/internal/zoom-support.svelte.ts delete mode 100644 web/src/lib/stores/zoom-image.store.ts diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts deleted file mode 100644 index 29074fc7b0..0000000000 --- a/web/src/lib/actions/zoom-image.ts +++ /dev/null @@ -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(); - } - }, - }; -}; diff --git a/web/src/lib/components/album-page/album-map.svelte b/web/src/lib/components/album-page/album-map.svelte index bb500f9a5e..76cd0ec050 100644 --- a/web/src/lib/components/album-page/album-map.svelte +++ b/web/src/lib/components/album-page/album-map.svelte @@ -1,5 +1,5 @@ @@ -217,27 +148,32 @@ { shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false }, ]} /> -{#if imageError} +{#if assetManager.loadError}
{/if} - -
- - {#if !imageLoaded} + mediaLoaded(assetManager)} + onerror={() => mediaLoadError(assetManager)} + /> + {#if !assetManager.isLoaded}
- {:else if !imageError} + {:else if !assetManager.loadError}
({})} onswipe={onSwipe} class="h-full w-full" @@ -245,7 +181,7 @@ > {#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground} - {#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox} + {#each getBoundingBox($boundingBoxesArray, zoomImageState!, $photoViewerImgElement) as boundingbox}
{#if isFaceEditMode.value} - + {/if} {/if}
diff --git a/web/src/lib/components/asset-viewer/video-native-viewer.svelte b/web/src/lib/components/asset-viewer/video-native-viewer.svelte index 4b8bb40f77..2a6218afaf 100644 --- a/web/src/lib/components/asset-viewer/video-native-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-native-viewer.svelte @@ -3,6 +3,7 @@ import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte'; import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte'; import { assetViewerFadeDuration } from '$lib/constants'; + import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { loopVideo as loopVideoPreference, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store'; @@ -16,9 +17,8 @@ import { fade } from 'svelte/transition'; interface Props { - assetId: string; + assetManager: AssetManager; loopVideo: boolean; - cacheKey: string | null; onPreviousAsset?: () => void; onNextAsset?: () => void; onVideoEnded?: () => void; @@ -27,9 +27,8 @@ } let { - assetId, + assetManager = $bindable(), loopVideo, - cacheKey, onPreviousAsset = () => {}, onNextAsset = () => {}, onVideoEnded = () => {}, diff --git a/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte index 2cf8e65871..65af34e321 100644 --- a/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-panorama-viewer.svelte @@ -3,12 +3,13 @@ import { t } from 'svelte-i18n'; import { fade } from 'svelte/transition'; import LoadingSpinner from '../shared-components/loading-spinner.svelte'; + import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte'; interface Props { - assetId: string; + assetManager: AssetManager; } - const { assetId }: Props = $props(); + const { assetManager = $bindable() }: Props = $props(); const modules = Promise.all([ import('./photo-sphere-viewer-adapter.svelte').then((module) => module.default), diff --git a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte index a5a94d85d4..b243b6ae55 100644 --- a/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte +++ b/web/src/lib/components/asset-viewer/video-wrapper-viewer.svelte @@ -1,12 +1,12 @@ {#if projectionType === ProjectionType.EQUIRECTANGULAR} - + {:else} { - const laterAsset = await timelineManager.getLaterAsset(asset); - - if (laterAsset) { - // TODO: If preloadAsset is undefined, throw an exception. - // assetManager.preloadAssets = [await timelineManager.getLaterAsset(laterAsset)]; - await navigate({ targetRoute: 'current', assetId: laterAsset.id }); + let laterAsset = undefined; + if (asset) { + laterAsset = await timelineManager.getLaterAsset(asset); + if (laterAsset) { + // TODO: If preloadAsset is undefined, throw an exception. + // assetManager.preloadAssets = [await timelineManager.getLaterAsset(laterAsset)]; + await navigate({ targetRoute: 'current', assetId: laterAsset.id }); + } } - return !!laterAsset; }; const handleNext = async () => { - const earlierAsset = await timelineManager.getEarlierAsset(asset); - - if (earlierAsset) { - // assetManager.preloadAssets = [await timelineManager.getEarlierAsset(earlierAsset)]; - await navigate({ targetRoute: 'current', assetId: earlierAsset.id }); + let earlierAsset = undefined; + if (asset) { + earlierAsset = await timelineManager.getEarlierAsset(asset); + if (earlierAsset) { + // assetManager.preloadAssets = [await timelineManager.getEarlierAsset(earlierAsset)]; + await navigate({ targetRoute: 'current', assetId: earlierAsset.id }); + } } - return !!earlierAsset; }; const handleRandom = async () => { - const randomAsset = await timelineManager.getRandomAsset(); - - if (randomAsset) { - await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + let randomAsset = undefined; + if (asset) { + randomAsset = await timelineManager.getRandomAsset(); + if (randomAsset) { + await navigate({ targetRoute: 'current', assetId: randomAsset.id }); + } } - return !!randomAsset; }; @@ -777,7 +780,7 @@ }); $effect(() => { - if (assetManager.showAssetViewer) { + if (assetManager.showAssetViewer && asset) { const { localDateTime } = getTimes(asset.fileCreatedAt, DateTime.local().offset / 60); void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month }); } diff --git a/web/src/lib/components/share-page/individual-shared-viewer.svelte b/web/src/lib/components/share-page/individual-shared-viewer.svelte index 2b955812bd..6e925e99e0 100644 --- a/web/src/lib/components/share-page/individual-shared-viewer.svelte +++ b/web/src/lib/components/share-page/individual-shared-viewer.svelte @@ -3,7 +3,7 @@ import type { Action } from '$lib/components/asset-viewer/actions/action'; import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte'; 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 type { Viewport } from '$lib/managers/timeline-manager/types'; import { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; diff --git a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte index 34e5252b18..9a1e8363db 100644 --- a/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte +++ b/web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte @@ -4,7 +4,7 @@ import type { Action } from '$lib/components/asset-viewer/actions/action'; import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte'; 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 type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types'; import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte'; diff --git a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte index fb14c56e09..3a1171c8c0 100644 --- a/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte +++ b/web/src/lib/components/utilities-page/duplicates/duplicates-compare-control.svelte @@ -2,7 +2,7 @@ import { shortcuts } from '$lib/actions/shortcut'; import Portal from '$lib/components/shared-components/portal/portal.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 { suggestDuplicate } from '$lib/utils/duplicate-utils'; import { navigate } from '$lib/utils/navigation'; diff --git a/web/src/lib/managers/asset-manager.svelte.ts b/web/src/lib/managers/asset-manager.svelte.ts deleted file mode 100644 index b294a02d1a..0000000000 --- a/web/src/lib/managers/asset-manager.svelte.ts +++ /dev/null @@ -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) { - 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 }); - } -} diff --git a/web/src/lib/managers/asset-manager/asset-manager.svelte.ts b/web/src/lib/managers/asset-manager/asset-manager.svelte.ts new file mode 100644 index 0000000000..5550ff4b44 --- /dev/null +++ b/web/src/lib/managers/asset-manager/asset-manager.svelte.ts @@ -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 }); +// }); diff --git a/web/src/lib/managers/asset-manager/internal/load-support.svelte.ts b/web/src/lib/managers/asset-manager/internal/load-support.svelte.ts new file mode 100644 index 0000000000..eebf28e120 --- /dev/null +++ b/web/src/lib/managers/asset-manager/internal/load-support.svelte.ts @@ -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; +} diff --git a/web/src/lib/managers/asset-manager/internal/zoom-support.svelte.ts b/web/src/lib/managers/asset-manager/internal/zoom-support.svelte.ts new file mode 100644 index 0000000000..f07959dd15 --- /dev/null +++ b/web/src/lib/managers/asset-manager/internal/zoom-support.svelte.ts @@ -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 { + 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(); + }; + }; +} diff --git a/web/src/lib/stores/zoom-image.store.ts b/web/src/lib/stores/zoom-image.store.ts deleted file mode 100644 index 2c6ee18972..0000000000 --- a/web/src/lib/stores/zoom-image.store.ts +++ /dev/null @@ -1,4 +0,0 @@ -import type { ZoomImageWheelState } from '@zoom-image/core'; -import { writable } from 'svelte/store'; - -export const photoZoomState = writable(); diff --git a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte index fe9d3bd718..e0942fed5a 100644 --- a/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/albums/[albumId=id]/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -34,7 +34,7 @@ import UserAvatar from '$lib/components/shared-components/user-avatar.svelte'; import { AlbumPageViewMode, AppRoute } from '$lib/constants'; 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 { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; diff --git a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8d9a2fca3f..2ae67da6d4 100644 --- a/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/archive/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -14,7 +14,7 @@ import { AssetAction } from '$lib/constants'; 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetVisibility } from '@immich/sdk'; diff --git a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3684fa5599..9f265323c8 100644 --- a/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/favorites/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -17,7 +17,7 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { preferences } from '$lib/stores/user.store'; diff --git a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte index 3fdd7268b4..896bb00391 100644 --- a/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/folders/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -32,7 +32,7 @@ import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js'; import { t } from 'svelte-i18n'; 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'; interface Props { diff --git a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte index 8a4346bf8d..b2e471ffa8 100644 --- a/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/locked/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -12,7 +12,7 @@ import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte'; import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte'; 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import { AssetVisibility, lockAuthSession } from '@immich/sdk'; diff --git a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte index 24ddd09665..c77ceb010c 100644 --- a/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/map/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -4,7 +4,7 @@ import Map from '$lib/components/shared-components/map/map.svelte'; import Portal from '$lib/components/shared-components/portal/portal.svelte'; 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 { handlePromiseError } from '$lib/utils'; import { navigate } from '$lib/utils/navigation'; diff --git a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte index 1503bfa7fe..9c169a676c 100644 --- a/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/memory/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -1,6 +1,6 @@