diff --git a/web/src/lib/actions/zoom-image.ts b/web/src/lib/actions/zoom-image.ts index 36cce538d1..20eabd892b 100644 --- a/web/src/lib/actions/zoom-image.ts +++ b/web/src/lib/actions/zoom-image.ts @@ -1,19 +1,12 @@ -import { photoZoomState } from '$lib/stores/zoom-image.store'; +import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { createZoomImageWheel } from '@zoom-image/core'; -import { get } from 'svelte/store'; export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => { - const state = get(photoZoomState); - const zoomInstance = createZoomImageWheel(node, { - maxZoom: 10, - initialState: state, - }); + const zoomInstance = createZoomImageWheel(node, { maxZoom: 10, initialState: assetViewerManager.zoomState }); const unsubscribes = [ - photoZoomState.subscribe((state) => zoomInstance.setState(state)), - zoomInstance.subscribe(({ state }) => { - photoZoomState.set(state); - }), + assetViewerManager.on('ZoomChange', (state) => zoomInstance.setState(state)), + zoomInstance.subscribe(({ state }) => assetViewerManager.onZoomChange(state)), ]; const stopIfDisabled = (event: Event) => { diff --git a/web/src/lib/components/AssetViewerEvents.svelte b/web/src/lib/components/AssetViewerEvents.svelte new file mode 100644 index 0000000000..a33b74aab5 --- /dev/null +++ b/web/src/lib/components/AssetViewerEvents.svelte @@ -0,0 +1,30 @@ + diff --git a/web/src/lib/components/OnEvents.svelte b/web/src/lib/components/OnEvents.svelte index 3933f4df7b..7f8039e6e3 100644 --- a/web/src/lib/components/OnEvents.svelte +++ b/web/src/lib/components/OnEvents.svelte @@ -2,9 +2,9 @@ import { eventManager, type Events } from '$lib/managers/event-manager.svelte'; import { onMount } from 'svelte'; - type Props = Partial<{ - [K in keyof Events as `on${K}`]: (...args: Events[K]) => void; - }>; + type Props = { + [K in keyof Events as `on${K}`]?: (...args: Events[K]) => void; + }; const props: Props = $props(); const unsubscribes: Array<() => void> = []; diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts index 5ee6dbf93e..68de2509be 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.spec.ts @@ -8,15 +8,8 @@ import AssetViewerNavBar from './asset-viewer-nav-bar.svelte'; describe('AssetViewerNavBar component', () => { const additionalProps = { - showCopyButton: false, - showZoomButton: false, - showDownloadButton: false, - showMotionPlayButton: false, - showShareButton: false, preAction: () => {}, - onZoomImage: () => {}, onAction: () => {}, - onEdit: () => {}, onPlaySlideshow: () => {}, onClose: () => {}, playOriginalVideo: false, diff --git a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte index bd20d6efeb..6754ad70cf 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer-nav-bar.svelte @@ -23,12 +23,9 @@ import { Route } from '$lib/route'; import { getGlobalActions } from '$lib/services/app.service'; import { getAssetActions, handleReplaceAsset } from '$lib/services/asset.service'; - import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { user } from '$lib/stores/user.store'; - import { photoZoomState } from '$lib/stores/zoom-image.store'; import { getSharedLink, withoutIcons } from '$lib/utils'; import type { OnUndoDelete } from '$lib/utils/actions'; - import { canCopyImageToClipboard } from '$lib/utils/asset-utils'; import { toTimelineAsset } from '$lib/utils/timeline-util'; import { AssetTypeEnum, @@ -38,15 +35,12 @@ type PersonResponseDto, type StackResponseDto, } from '@immich/sdk'; - import { CommandPaletteDefaultProvider, IconButton, type ActionItem } from '@immich/ui'; + import { CommandPaletteDefaultProvider, type ActionItem } from '@immich/ui'; import { mdiArrowLeft, mdiCompare, - mdiContentCopy, mdiDotsVertical, mdiImageSearch, - mdiMagnifyMinusOutline, - mdiMagnifyPlusOutline, mdiPresentationPlay, mdiUpload, mdiVideoOutline, @@ -59,8 +53,6 @@ person?: PersonResponseDto | null; stack?: StackResponseDto | null; showSlideshow?: boolean; - onZoomImage: () => void; - onCopyImage?: () => Promise; preAction: PreAction; onAction: OnAction; onUndoDelete?: OnUndoDelete; @@ -76,8 +68,6 @@ person = null, stack = null, showSlideshow = false, - onZoomImage, - onCopyImage, preAction, onAction, onUndoDelete = undefined, @@ -89,35 +79,18 @@ const isOwner = $derived($user && asset.ownerId === $user?.id); const isLocked = $derived(asset.visibility === AssetVisibility.Locked); - const isImage = $derived(asset.type === AssetTypeEnum.Image); const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch); const { Cast } = $derived(getGlobalActions($t)); - const { Close, ZoomIn, ZoomOut } = $derived({ - Close: { - title: $t('go_back'), - type: $t('assets'), - icon: mdiArrowLeft, - $if: () => !!onClose, - onAction: () => onClose?.(), - shortcuts: [{ key: 'Escape' }], - }, - - ZoomIn: { - title: $t('zoom_image'), - icon: mdiMagnifyPlusOutline, - $if: () => isImage && $photoZoomState && $photoZoomState.currentZoom <= 1, - onAction: () => onZoomImage(), - }, - - ZoomOut: { - title: $t('zoom_image'), - icon: mdiMagnifyMinusOutline, - $if: () => $photoZoomState && $photoZoomState.currentZoom > 1, - onAction: () => onZoomImage(), - }, - } satisfies Record); + const Close: ActionItem = $derived({ + title: $t('go_back'), + type: $t('assets'), + icon: mdiArrowLeft, + $if: () => !!onClose, + onAction: () => onClose?.(), + shortcuts: [{ key: 'Escape' }], + }); const { Share, @@ -129,6 +102,9 @@ Unfavorite, PlayMotionPhoto, StopMotionPhoto, + ZoomIn, + ZoomOut, + Copy, Info, Edit, RefreshFacesJob, @@ -153,6 +129,9 @@ Unfavorite, PlayMotionPhoto, StopMotionPhoto, + ZoomIn, + ZoomOut, + Copy, Info, Edit, RefreshFacesJob, @@ -177,18 +156,7 @@ - - {#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image && $photoViewerImgElement} - onCopyImage?.()} - /> - {/if} - + diff --git a/web/src/lib/components/asset-viewer/asset-viewer.svelte b/web/src/lib/components/asset-viewer/asset-viewer.svelte index 9661fb3aed..15ff8ea57f 100644 --- a/web/src/lib/components/asset-viewer/asset-viewer.svelte +++ b/web/src/lib/components/asset-viewer/asset-viewer.svelte @@ -70,7 +70,6 @@ onUndoDelete?: OnUndoDelete; onClose?: (asset: AssetResponseDto) => void; onRandom?: () => Promise<{ id: string } | undefined>; - copyImage?: () => Promise; } let { @@ -86,7 +85,6 @@ onUndoDelete, onClose, onRandom, - copyImage = $bindable(), }: Props = $props(); const { setAssetId } = assetViewingStore; @@ -110,7 +108,6 @@ let unsubscribes: (() => void)[] = []; let stack: StackResponseDto | null = $state(null); - let zoomToggle = $state(() => void 0); let playOriginalVideo = $state($alwaysLoadOriginalVideo); const setPlayOriginalVideo = (value: boolean) => { @@ -445,8 +442,6 @@ {person} {stack} showSlideshow={true} - onZoomImage={zoomToggle} - onCopyImage={copyImage} preAction={handlePreAction} onAction={handleAction} {onUndoDelete} @@ -481,8 +476,6 @@
{#if viewerKind === 'StackPhotoViewer'} navigateAsset('previous')} onNextAsset={() => navigateAsset('next')} @@ -515,13 +508,11 @@ {playOriginalVideo} /> {:else if viewerKind === 'ImagePanaramaViewer'} - + {:else if viewerKind === 'CropArea'} {:else if viewerKind === 'PhotoViewer'} navigateAsset('previous')} onNextAsset={() => navigateAsset('next')} diff --git a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte index 196f5ec6c5..5b18dbb4e3 100644 --- a/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte +++ b/web/src/lib/components/asset-viewer/image-panorama-viewer.svelte @@ -8,10 +8,9 @@ type Props = { asset: AssetResponseDto; - zoomToggle?: (() => void) | null; }; - let { asset, zoomToggle = $bindable() }: Props = $props(); + let { asset }: Props = $props(); const loadAssetData = async (id: string) => { const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview }); @@ -23,7 +22,7 @@ {#await Promise.all([loadAssetData(asset.id), import('./photo-sphere-viewer-adapter.svelte')])} {:then [data, { default: PhotoSphereViewer }]} - + {:catch} {$t('errors.failed_to_load_asset')} {/await} diff --git a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte index 2e5d0d85c5..f671aa1b1c 100644 --- a/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte +++ b/web/src/lib/components/asset-viewer/photo-sphere-viewer-adapter.svelte @@ -1,8 +1,9 @@ - + + +
diff --git a/web/src/lib/components/asset-viewer/photo-viewer.svelte b/web/src/lib/components/asset-viewer/photo-viewer.svelte index afb353c7c6..0a44505f40 100644 --- a/web/src/lib/components/asset-viewer/photo-viewer.svelte +++ b/web/src/lib/components/asset-viewer/photo-viewer.svelte @@ -4,15 +4,15 @@ import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte'; import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte'; import BrokenAsset from '$lib/components/assets/broken-asset.svelte'; + import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte'; import { assetViewerFadeDuration } from '$lib/constants'; + import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte'; import { castManager } from '$lib/managers/cast-manager.svelte'; import { imageManager } from '$lib/managers/ImageManager.svelte'; - import { photoViewerImgElement } from '$lib/stores/assets-store.svelte'; import { isFaceEditMode } from '$lib/stores/face-edit.svelte'; import { ocrManager } from '$lib/stores/ocr.svelte'; import { boundingBoxesArray } from '$lib/stores/people.store'; import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store'; - import { photoZoomState } from '$lib/stores/zoom-image.store'; import { getAssetUrl, targetImageSize as getTargetImageSize, handlePromiseError } from '$lib/utils'; import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils'; import { handleError } from '$lib/utils/handle-error'; @@ -35,8 +35,6 @@ sharedLink?: SharedLinkResponseDto | undefined; onPreviousAsset?: (() => void) | null; onNextAsset?: (() => void) | null; - copyImage?: () => Promise; - zoomToggle?: (() => void) | null; } let { @@ -46,8 +44,6 @@ sharedLink = undefined, onPreviousAsset = null, onNextAsset = null, - copyImage = $bindable(), - zoomToggle = $bindable(), }: Props = $props(); const { slideshowState, slideshowLook } = slideshowStore; @@ -59,64 +55,63 @@ let loader = $state(); - photoZoomState.set({ + assetViewerManager.zoomState = { currentRotation: 0, currentZoom: 1, enable: true, currentPositionX: 0, currentPositionY: 0, - }); + }; onDestroy(() => { $boundingBoxesArray = []; }); let ocrBoxes = $derived( - ocrManager.showOverlay && $photoViewerImgElement - ? getOcrBoundingBoxes(ocrManager.data, $photoZoomState, $photoViewerImgElement) + ocrManager.showOverlay && assetViewerManager.imgRef + ? getOcrBoundingBoxes(ocrManager.data, assetViewerManager.zoomState, assetViewerManager.imgRef) : [], ); let isOcrActive = $derived(ocrManager.showOverlay); - copyImage = async () => { - if (!canCopyImageToClipboard() || !$photoViewerImgElement) { + const onCopy = async () => { + if (!canCopyImageToClipboard() || !assetViewerManager.imgRef) { return; } try { - await copyImageToClipboard($photoViewerImgElement); + await copyImageToClipboard(assetViewerManager.imgRef); toastManager.info($t('copied_image_to_clipboard')); } catch (error) { handleError(error, $t('copy_error')); } }; - zoomToggle = () => { - photoZoomState.set({ - ...$photoZoomState, - currentZoom: $photoZoomState.currentZoom > 1 ? 1 : 2, - }); + const onZoom = () => { + assetViewerManager.zoom = assetViewerManager.zoom > 1 ? 1 : 2; }; const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow); $effect(() => { - if (isFaceEditMode.value && $photoZoomState.currentZoom > 1) { - zoomToggle(); + if (isFaceEditMode.value && assetViewerManager.zoom > 1) { + onZoom(); } }); + // TODO move to action + command palette const onCopyShortcut = (event: KeyboardEvent) => { if (globalThis.getSelection()?.type === 'Range') { return; } event.preventDefault(); - handlePromiseError(copyImage()); + + handlePromiseError(onCopy()); }; const onSwipe = (event: SwipeCustomEvent) => { - if ($photoZoomState.currentZoom > 1) { + if (assetViewerManager.zoom > 1) { return; } @@ -133,7 +128,7 @@ } }; - const targetImageSize = $derived(getTargetImageSize(asset, originalImageLoaded || $photoZoomState.currentZoom > 1)); + const targetImageSize = $derived(getTargetImageSize(asset, originalImageLoaded || assetViewerManager.zoom > 1)); $effect(() => { if (imageLoaderUrl) { @@ -167,7 +162,7 @@ onDestroy(() => imageManager.cancelPreloadUrl(imageLoaderUrl)); let imageLoaderUrl = $derived( - getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || $photoZoomState.currentZoom > 1 }), + getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || assetViewerManager.zoom > 1 }), ); let containerWidth = $state(0); @@ -187,13 +182,14 @@ }); + + {#if imageError} @@ -228,7 +224,7 @@ /> {/if} {$getAltText(toTimelineAsset(asset))} - {#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox} + {#each getBoundingBox($boundingBoxesArray, assetViewerManager.zoomState, assetViewerManager.imgRef) as boundingbox}
{#if isFaceEditMode.value} - + {/if} {/if}
diff --git a/web/src/lib/components/faces-page/assign-face-side-panel.svelte b/web/src/lib/components/faces-page/assign-face-side-panel.svelte index ac78623071..772fbe954e 100644 --- a/web/src/lib/components/faces-page/assign-face-side-panel.svelte +++ b/web/src/lib/components/faces-page/assign-face-side-panel.svelte @@ -1,7 +1,7 @@