mirror of
https://github.com/immich-app/immich.git
synced 2026-05-18 03:10:24 +03:00
refactor: asset navbar (#25480)
This commit is contained in:
@@ -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 { createZoomImageWheel } from '@zoom-image/core';
|
||||||
import { get } from 'svelte/store';
|
|
||||||
|
|
||||||
export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => {
|
export const zoomImageAction = (node: HTMLElement, options?: { disabled?: boolean }) => {
|
||||||
const state = get(photoZoomState);
|
const zoomInstance = createZoomImageWheel(node, { maxZoom: 10, initialState: assetViewerManager.zoomState });
|
||||||
const zoomInstance = createZoomImageWheel(node, {
|
|
||||||
maxZoom: 10,
|
|
||||||
initialState: state,
|
|
||||||
});
|
|
||||||
|
|
||||||
const unsubscribes = [
|
const unsubscribes = [
|
||||||
photoZoomState.subscribe((state) => zoomInstance.setState(state)),
|
assetViewerManager.on('ZoomChange', (state) => zoomInstance.setState(state)),
|
||||||
zoomInstance.subscribe(({ state }) => {
|
zoomInstance.subscribe(({ state }) => assetViewerManager.onZoomChange(state)),
|
||||||
photoZoomState.set(state);
|
|
||||||
}),
|
|
||||||
];
|
];
|
||||||
|
|
||||||
const stopIfDisabled = (event: Event) => {
|
const stopIfDisabled = (event: Event) => {
|
||||||
|
|||||||
@@ -0,0 +1,30 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { assetViewerManager, type Events } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
|
import type { EventCallback } from '$lib/utils/base-event-manager.svelte';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
[K in keyof Events as `on${K}`]?: EventCallback<Events, K>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const props: Props = $props();
|
||||||
|
const unsubscribes: Array<() => void> = [];
|
||||||
|
|
||||||
|
onMount(() => {
|
||||||
|
for (const name of Object.keys(props)) {
|
||||||
|
const event = name.slice(2) as keyof Events;
|
||||||
|
const listener = props[name as keyof Props] as EventCallback<Events, typeof event> | undefined;
|
||||||
|
if (!listener) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unsubscribes.push(assetViewerManager.on(event, listener));
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
for (const unsubscribe of unsubscribes) {
|
||||||
|
unsubscribe();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -2,9 +2,9 @@
|
|||||||
import { eventManager, type Events } from '$lib/managers/event-manager.svelte';
|
import { eventManager, type Events } from '$lib/managers/event-manager.svelte';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
|
||||||
type Props = Partial<{
|
type Props = {
|
||||||
[K in keyof Events as `on${K}`]: (...args: Events[K]) => void;
|
[K in keyof Events as `on${K}`]?: (...args: Events[K]) => void;
|
||||||
}>;
|
};
|
||||||
|
|
||||||
const props: Props = $props();
|
const props: Props = $props();
|
||||||
const unsubscribes: Array<() => void> = [];
|
const unsubscribes: Array<() => void> = [];
|
||||||
|
|||||||
@@ -8,15 +8,8 @@ import AssetViewerNavBar from './asset-viewer-nav-bar.svelte';
|
|||||||
|
|
||||||
describe('AssetViewerNavBar component', () => {
|
describe('AssetViewerNavBar component', () => {
|
||||||
const additionalProps = {
|
const additionalProps = {
|
||||||
showCopyButton: false,
|
|
||||||
showZoomButton: false,
|
|
||||||
showDownloadButton: false,
|
|
||||||
showMotionPlayButton: false,
|
|
||||||
showShareButton: false,
|
|
||||||
preAction: () => {},
|
preAction: () => {},
|
||||||
onZoomImage: () => {},
|
|
||||||
onAction: () => {},
|
onAction: () => {},
|
||||||
onEdit: () => {},
|
|
||||||
onPlaySlideshow: () => {},
|
onPlaySlideshow: () => {},
|
||||||
onClose: () => {},
|
onClose: () => {},
|
||||||
playOriginalVideo: false,
|
playOriginalVideo: false,
|
||||||
|
|||||||
@@ -23,12 +23,9 @@
|
|||||||
import { Route } from '$lib/route';
|
import { Route } from '$lib/route';
|
||||||
import { getGlobalActions } from '$lib/services/app.service';
|
import { getGlobalActions } from '$lib/services/app.service';
|
||||||
import { getAssetActions, handleReplaceAsset } from '$lib/services/asset.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 { user } from '$lib/stores/user.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
|
||||||
import { getSharedLink, withoutIcons } from '$lib/utils';
|
import { getSharedLink, withoutIcons } from '$lib/utils';
|
||||||
import type { OnUndoDelete } from '$lib/utils/actions';
|
import type { OnUndoDelete } from '$lib/utils/actions';
|
||||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import {
|
import {
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
@@ -38,15 +35,12 @@
|
|||||||
type PersonResponseDto,
|
type PersonResponseDto,
|
||||||
type StackResponseDto,
|
type StackResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { CommandPaletteDefaultProvider, IconButton, type ActionItem } from '@immich/ui';
|
import { CommandPaletteDefaultProvider, type ActionItem } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiArrowLeft,
|
mdiArrowLeft,
|
||||||
mdiCompare,
|
mdiCompare,
|
||||||
mdiContentCopy,
|
|
||||||
mdiDotsVertical,
|
mdiDotsVertical,
|
||||||
mdiImageSearch,
|
mdiImageSearch,
|
||||||
mdiMagnifyMinusOutline,
|
|
||||||
mdiMagnifyPlusOutline,
|
|
||||||
mdiPresentationPlay,
|
mdiPresentationPlay,
|
||||||
mdiUpload,
|
mdiUpload,
|
||||||
mdiVideoOutline,
|
mdiVideoOutline,
|
||||||
@@ -59,8 +53,6 @@
|
|||||||
person?: PersonResponseDto | null;
|
person?: PersonResponseDto | null;
|
||||||
stack?: StackResponseDto | null;
|
stack?: StackResponseDto | null;
|
||||||
showSlideshow?: boolean;
|
showSlideshow?: boolean;
|
||||||
onZoomImage: () => void;
|
|
||||||
onCopyImage?: () => Promise<void>;
|
|
||||||
preAction: PreAction;
|
preAction: PreAction;
|
||||||
onAction: OnAction;
|
onAction: OnAction;
|
||||||
onUndoDelete?: OnUndoDelete;
|
onUndoDelete?: OnUndoDelete;
|
||||||
@@ -76,8 +68,6 @@
|
|||||||
person = null,
|
person = null,
|
||||||
stack = null,
|
stack = null,
|
||||||
showSlideshow = false,
|
showSlideshow = false,
|
||||||
onZoomImage,
|
|
||||||
onCopyImage,
|
|
||||||
preAction,
|
preAction,
|
||||||
onAction,
|
onAction,
|
||||||
onUndoDelete = undefined,
|
onUndoDelete = undefined,
|
||||||
@@ -89,35 +79,18 @@
|
|||||||
|
|
||||||
const isOwner = $derived($user && asset.ownerId === $user?.id);
|
const isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||||
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
const isLocked = $derived(asset.visibility === AssetVisibility.Locked);
|
||||||
const isImage = $derived(asset.type === AssetTypeEnum.Image);
|
|
||||||
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
const smartSearchEnabled = $derived(featureFlagsManager.value.smartSearch);
|
||||||
|
|
||||||
const { Cast } = $derived(getGlobalActions($t));
|
const { Cast } = $derived(getGlobalActions($t));
|
||||||
|
|
||||||
const { Close, ZoomIn, ZoomOut } = $derived({
|
const Close: ActionItem = $derived({
|
||||||
Close: {
|
|
||||||
title: $t('go_back'),
|
title: $t('go_back'),
|
||||||
type: $t('assets'),
|
type: $t('assets'),
|
||||||
icon: mdiArrowLeft,
|
icon: mdiArrowLeft,
|
||||||
$if: () => !!onClose,
|
$if: () => !!onClose,
|
||||||
onAction: () => onClose?.(),
|
onAction: () => onClose?.(),
|
||||||
shortcuts: [{ key: 'Escape' }],
|
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<string, ActionItem>);
|
|
||||||
|
|
||||||
const {
|
const {
|
||||||
Share,
|
Share,
|
||||||
@@ -129,6 +102,9 @@
|
|||||||
Unfavorite,
|
Unfavorite,
|
||||||
PlayMotionPhoto,
|
PlayMotionPhoto,
|
||||||
StopMotionPhoto,
|
StopMotionPhoto,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Copy,
|
||||||
Info,
|
Info,
|
||||||
Edit,
|
Edit,
|
||||||
RefreshFacesJob,
|
RefreshFacesJob,
|
||||||
@@ -153,6 +129,9 @@
|
|||||||
Unfavorite,
|
Unfavorite,
|
||||||
PlayMotionPhoto,
|
PlayMotionPhoto,
|
||||||
StopMotionPhoto,
|
StopMotionPhoto,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Copy,
|
||||||
Info,
|
Info,
|
||||||
Edit,
|
Edit,
|
||||||
RefreshFacesJob,
|
RefreshFacesJob,
|
||||||
@@ -177,18 +156,7 @@
|
|||||||
<ActionButton action={StopMotionPhoto} />
|
<ActionButton action={StopMotionPhoto} />
|
||||||
<ActionButton action={ZoomIn} />
|
<ActionButton action={ZoomIn} />
|
||||||
<ActionButton action={ZoomOut} />
|
<ActionButton action={ZoomOut} />
|
||||||
|
<ActionButton action={Copy} />
|
||||||
{#if canCopyImageToClipboard() && asset.type === AssetTypeEnum.Image && $photoViewerImgElement}
|
|
||||||
<IconButton
|
|
||||||
color="secondary"
|
|
||||||
variant="ghost"
|
|
||||||
shape="round"
|
|
||||||
icon={mdiContentCopy}
|
|
||||||
aria-label={$t('copy_image')}
|
|
||||||
onclick={() => onCopyImage?.()}
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<ActionButton action={SharedLinkDownload} />
|
<ActionButton action={SharedLinkDownload} />
|
||||||
<ActionButton action={Info} />
|
<ActionButton action={Info} />
|
||||||
<ActionButton action={Favorite} />
|
<ActionButton action={Favorite} />
|
||||||
|
|||||||
@@ -70,7 +70,6 @@
|
|||||||
onUndoDelete?: OnUndoDelete;
|
onUndoDelete?: OnUndoDelete;
|
||||||
onClose?: (asset: AssetResponseDto) => void;
|
onClose?: (asset: AssetResponseDto) => void;
|
||||||
onRandom?: () => Promise<{ id: string } | undefined>;
|
onRandom?: () => Promise<{ id: string } | undefined>;
|
||||||
copyImage?: () => Promise<void>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -86,7 +85,6 @@
|
|||||||
onUndoDelete,
|
onUndoDelete,
|
||||||
onClose,
|
onClose,
|
||||||
onRandom,
|
onRandom,
|
||||||
copyImage = $bindable(),
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const { setAssetId } = assetViewingStore;
|
const { setAssetId } = assetViewingStore;
|
||||||
@@ -110,7 +108,6 @@
|
|||||||
let unsubscribes: (() => void)[] = [];
|
let unsubscribes: (() => void)[] = [];
|
||||||
let stack: StackResponseDto | null = $state(null);
|
let stack: StackResponseDto | null = $state(null);
|
||||||
|
|
||||||
let zoomToggle = $state(() => void 0);
|
|
||||||
let playOriginalVideo = $state($alwaysLoadOriginalVideo);
|
let playOriginalVideo = $state($alwaysLoadOriginalVideo);
|
||||||
|
|
||||||
const setPlayOriginalVideo = (value: boolean) => {
|
const setPlayOriginalVideo = (value: boolean) => {
|
||||||
@@ -445,8 +442,6 @@
|
|||||||
{person}
|
{person}
|
||||||
{stack}
|
{stack}
|
||||||
showSlideshow={true}
|
showSlideshow={true}
|
||||||
onZoomImage={zoomToggle}
|
|
||||||
onCopyImage={copyImage}
|
|
||||||
preAction={handlePreAction}
|
preAction={handlePreAction}
|
||||||
onAction={handleAction}
|
onAction={handleAction}
|
||||||
{onUndoDelete}
|
{onUndoDelete}
|
||||||
@@ -481,8 +476,6 @@
|
|||||||
<div class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
|
<div class="z-[-1] relative col-start-1 col-span-4 row-start-1 row-span-full">
|
||||||
{#if viewerKind === 'StackPhotoViewer'}
|
{#if viewerKind === 'StackPhotoViewer'}
|
||||||
<PhotoViewer
|
<PhotoViewer
|
||||||
bind:zoomToggle
|
|
||||||
bind:copyImage
|
|
||||||
cursor={{ ...cursor, current: previewStackedAsset! }}
|
cursor={{ ...cursor, current: previewStackedAsset! }}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
onNextAsset={() => navigateAsset('next')}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
@@ -515,13 +508,11 @@
|
|||||||
{playOriginalVideo}
|
{playOriginalVideo}
|
||||||
/>
|
/>
|
||||||
{:else if viewerKind === 'ImagePanaramaViewer'}
|
{:else if viewerKind === 'ImagePanaramaViewer'}
|
||||||
<ImagePanoramaViewer bind:zoomToggle {asset} />
|
<ImagePanoramaViewer {asset} />
|
||||||
{:else if viewerKind === 'CropArea'}
|
{:else if viewerKind === 'CropArea'}
|
||||||
<CropArea {asset} />
|
<CropArea {asset} />
|
||||||
{:else if viewerKind === 'PhotoViewer'}
|
{:else if viewerKind === 'PhotoViewer'}
|
||||||
<PhotoViewer
|
<PhotoViewer
|
||||||
bind:zoomToggle
|
|
||||||
bind:copyImage
|
|
||||||
{cursor}
|
{cursor}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
onNextAsset={() => navigateAsset('next')}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
|
|||||||
@@ -8,10 +8,9 @@
|
|||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
asset: AssetResponseDto;
|
asset: AssetResponseDto;
|
||||||
zoomToggle?: (() => void) | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let { asset, zoomToggle = $bindable() }: Props = $props();
|
let { asset }: Props = $props();
|
||||||
|
|
||||||
const loadAssetData = async (id: string) => {
|
const loadAssetData = async (id: string) => {
|
||||||
const data = await viewAsset({ ...authManager.params, id, size: AssetMediaSize.Preview });
|
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')])}
|
{#await Promise.all([loadAssetData(asset.id), import('./photo-sphere-viewer-adapter.svelte')])}
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
{:then [data, { default: PhotoSphereViewer }]}
|
{:then [data, { default: PhotoSphereViewer }]}
|
||||||
<PhotoSphereViewer bind:zoomToggle panorama={data} originalPanorama={getAssetUrl({ asset, forceOriginal: true })} />
|
<PhotoSphereViewer panorama={data} originalPanorama={getAssetUrl({ asset, forceOriginal: true })} />
|
||||||
{:catch}
|
{:catch}
|
||||||
{$t('errors.failed_to_load_asset')}
|
{$t('errors.failed_to_load_asset')}
|
||||||
{/await}
|
{/await}
|
||||||
|
|||||||
@@ -1,8 +1,9 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
|
import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte';
|
||||||
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
|
import { boundingBoxesArray, type Faces } from '$lib/stores/people.store';
|
||||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
|
||||||
import {
|
import {
|
||||||
EquirectangularAdapter,
|
EquirectangularAdapter,
|
||||||
Viewer,
|
Viewer,
|
||||||
@@ -32,17 +33,9 @@
|
|||||||
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
adapter?: AdapterConstructor | [AdapterConstructor, unknown];
|
||||||
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
plugins?: (PluginConstructor | [PluginConstructor, unknown])[];
|
||||||
navbar?: boolean;
|
navbar?: boolean;
|
||||||
zoomToggle?: (() => void) | null;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let {
|
let { panorama, originalPanorama, adapter = EquirectangularAdapter, plugins = [], navbar = false }: Props = $props();
|
||||||
panorama,
|
|
||||||
originalPanorama,
|
|
||||||
adapter = EquirectangularAdapter,
|
|
||||||
plugins = [],
|
|
||||||
navbar = false,
|
|
||||||
zoomToggle = $bindable(),
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let container: HTMLDivElement | undefined = $state();
|
let container: HTMLDivElement | undefined = $state();
|
||||||
let viewer: Viewer;
|
let viewer: Viewer;
|
||||||
@@ -103,11 +96,8 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
zoomToggle = () => {
|
const onZoom = () => {
|
||||||
if (!viewer) {
|
viewer?.animate({ zoom: assetViewerManager.zoom > 1 ? 50 : 83.3, speed: 250 });
|
||||||
return;
|
|
||||||
}
|
|
||||||
viewer.animate({ zoom: $photoZoomState.currentZoom > 1 ? 50 : 83.3, speed: 250 });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let hasChangedResolution: boolean = false;
|
let hasChangedResolution: boolean = false;
|
||||||
@@ -156,11 +146,8 @@
|
|||||||
});
|
});
|
||||||
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
const resolutionPlugin = viewer.getPlugin<ResolutionPlugin>(ResolutionPlugin);
|
||||||
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
const zoomHandler = ({ zoomLevel }: events.ZoomUpdatedEvent) => {
|
||||||
// zoomLevel range: [0, 100]
|
// zoomLevel is 0-100
|
||||||
photoZoomState.set({
|
assetViewerManager.zoom = zoomLevel / 50;
|
||||||
...$photoZoomState,
|
|
||||||
currentZoom: zoomLevel / 50,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (Math.round(zoomLevel) >= 75 && !hasChangedResolution) {
|
if (Math.round(zoomLevel) >= 75 && !hasChangedResolution) {
|
||||||
// Replace the preview with the original
|
// Replace the preview with the original
|
||||||
@@ -181,13 +168,11 @@
|
|||||||
viewer.destroy();
|
viewer.destroy();
|
||||||
}
|
}
|
||||||
boundingBoxesUnsubscribe();
|
boundingBoxesUnsubscribe();
|
||||||
// zoomHandler is not called on initial load. Viewer initial zoom is 1, but photoZoomState could be != 1.
|
assetViewerManager.zoom = 1;
|
||||||
photoZoomState.set({
|
|
||||||
...$photoZoomState,
|
|
||||||
currentZoom: 1,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document use:shortcuts={[{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: true }]} />
|
<AssetViewerEvents {onZoom} />
|
||||||
|
|
||||||
|
<svelte:document use:shortcuts={[{ shortcut: { key: 'z' }, onShortcut: onZoom, preventDefault: true }]} />
|
||||||
<div class="h-full w-full mb-0" bind:this={container}></div>
|
<div class="h-full w-full mb-0" bind:this={container}></div>
|
||||||
|
|||||||
@@ -4,15 +4,15 @@
|
|||||||
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 OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte';
|
import OcrBoundingBox from '$lib/components/asset-viewer/ocr-bounding-box.svelte';
|
||||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||||
|
import AssetViewerEvents from '$lib/components/AssetViewerEvents.svelte';
|
||||||
import { assetViewerFadeDuration } from '$lib/constants';
|
import { assetViewerFadeDuration } from '$lib/constants';
|
||||||
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import { castManager } from '$lib/managers/cast-manager.svelte';
|
import { castManager } from '$lib/managers/cast-manager.svelte';
|
||||||
import { imageManager } from '$lib/managers/ImageManager.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 { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { ocrManager } from '$lib/stores/ocr.svelte';
|
import { ocrManager } from '$lib/stores/ocr.svelte';
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.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 { getAssetUrl, targetImageSize as getTargetImageSize, handlePromiseError } from '$lib/utils';
|
import { getAssetUrl, targetImageSize as getTargetImageSize, handlePromiseError } from '$lib/utils';
|
||||||
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
|
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
@@ -35,8 +35,6 @@
|
|||||||
sharedLink?: SharedLinkResponseDto | undefined;
|
sharedLink?: SharedLinkResponseDto | undefined;
|
||||||
onPreviousAsset?: (() => void) | null;
|
onPreviousAsset?: (() => void) | null;
|
||||||
onNextAsset?: (() => void) | null;
|
onNextAsset?: (() => void) | null;
|
||||||
copyImage?: () => Promise<void>;
|
|
||||||
zoomToggle?: (() => void) | null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
@@ -46,8 +44,6 @@
|
|||||||
sharedLink = undefined,
|
sharedLink = undefined,
|
||||||
onPreviousAsset = null,
|
onPreviousAsset = null,
|
||||||
onNextAsset = null,
|
onNextAsset = null,
|
||||||
copyImage = $bindable(),
|
|
||||||
zoomToggle = $bindable(),
|
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const { slideshowState, slideshowLook } = slideshowStore;
|
const { slideshowState, slideshowLook } = slideshowStore;
|
||||||
@@ -59,64 +55,63 @@
|
|||||||
|
|
||||||
let loader = $state<HTMLImageElement>();
|
let loader = $state<HTMLImageElement>();
|
||||||
|
|
||||||
photoZoomState.set({
|
assetViewerManager.zoomState = {
|
||||||
currentRotation: 0,
|
currentRotation: 0,
|
||||||
currentZoom: 1,
|
currentZoom: 1,
|
||||||
enable: true,
|
enable: true,
|
||||||
currentPositionX: 0,
|
currentPositionX: 0,
|
||||||
currentPositionY: 0,
|
currentPositionY: 0,
|
||||||
});
|
};
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$boundingBoxesArray = [];
|
$boundingBoxesArray = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
let ocrBoxes = $derived(
|
let ocrBoxes = $derived(
|
||||||
ocrManager.showOverlay && $photoViewerImgElement
|
ocrManager.showOverlay && assetViewerManager.imgRef
|
||||||
? getOcrBoundingBoxes(ocrManager.data, $photoZoomState, $photoViewerImgElement)
|
? getOcrBoundingBoxes(ocrManager.data, assetViewerManager.zoomState, assetViewerManager.imgRef)
|
||||||
: [],
|
: [],
|
||||||
);
|
);
|
||||||
|
|
||||||
let isOcrActive = $derived(ocrManager.showOverlay);
|
let isOcrActive = $derived(ocrManager.showOverlay);
|
||||||
|
|
||||||
copyImage = async () => {
|
const onCopy = async () => {
|
||||||
if (!canCopyImageToClipboard() || !$photoViewerImgElement) {
|
if (!canCopyImageToClipboard() || !assetViewerManager.imgRef) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await copyImageToClipboard($photoViewerImgElement);
|
await copyImageToClipboard(assetViewerManager.imgRef);
|
||||||
toastManager.info($t('copied_image_to_clipboard'));
|
toastManager.info($t('copied_image_to_clipboard'));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('copy_error'));
|
handleError(error, $t('copy_error'));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
zoomToggle = () => {
|
const onZoom = () => {
|
||||||
photoZoomState.set({
|
assetViewerManager.zoom = assetViewerManager.zoom > 1 ? 1 : 2;
|
||||||
...$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 && assetViewerManager.zoom > 1) {
|
||||||
zoomToggle();
|
onZoom();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// TODO move to action + command palette
|
||||||
const onCopyShortcut = (event: KeyboardEvent) => {
|
const onCopyShortcut = (event: KeyboardEvent) => {
|
||||||
if (globalThis.getSelection()?.type === 'Range') {
|
if (globalThis.getSelection()?.type === 'Range') {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
handlePromiseError(copyImage());
|
|
||||||
|
handlePromiseError(onCopy());
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSwipe = (event: SwipeCustomEvent) => {
|
const onSwipe = (event: SwipeCustomEvent) => {
|
||||||
if ($photoZoomState.currentZoom > 1) {
|
if (assetViewerManager.zoom > 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -133,7 +128,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const targetImageSize = $derived(getTargetImageSize(asset, originalImageLoaded || $photoZoomState.currentZoom > 1));
|
const targetImageSize = $derived(getTargetImageSize(asset, originalImageLoaded || assetViewerManager.zoom > 1));
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (imageLoaderUrl) {
|
if (imageLoaderUrl) {
|
||||||
@@ -167,7 +162,7 @@
|
|||||||
onDestroy(() => imageManager.cancelPreloadUrl(imageLoaderUrl));
|
onDestroy(() => imageManager.cancelPreloadUrl(imageLoaderUrl));
|
||||||
|
|
||||||
let imageLoaderUrl = $derived(
|
let imageLoaderUrl = $derived(
|
||||||
getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || $photoZoomState.currentZoom > 1 }),
|
getAssetUrl({ asset, sharedLink, forceOriginal: originalImageLoaded || assetViewerManager.zoom > 1 }),
|
||||||
);
|
);
|
||||||
|
|
||||||
let containerWidth = $state(0);
|
let containerWidth = $state(0);
|
||||||
@@ -187,13 +182,14 @@
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<AssetViewerEvents {onCopy} {onZoom} />
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
use:shortcuts={[
|
use:shortcuts={[
|
||||||
{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: true },
|
{ shortcut: { key: 'z' }, onShortcut: onZoom, preventDefault: true },
|
||||||
{ shortcut: { key: 's' }, onShortcut: onPlaySlideshow, preventDefault: true },
|
{ shortcut: { key: 's' }, onShortcut: onPlaySlideshow, preventDefault: true },
|
||||||
{ shortcut: { key: 'c', ctrl: true }, onShortcut: onCopyShortcut, preventDefault: false },
|
{ shortcut: { key: 'c', ctrl: true }, onShortcut: onCopyShortcut, preventDefault: false },
|
||||||
{ shortcut: { key: 'c', meta: true }, onShortcut: onCopyShortcut, preventDefault: false },
|
{ shortcut: { key: 'c', meta: true }, onShortcut: onCopyShortcut, preventDefault: false },
|
||||||
{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false },
|
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{#if imageError}
|
{#if imageError}
|
||||||
@@ -228,7 +224,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
<img
|
<img
|
||||||
bind:this={$photoViewerImgElement}
|
bind:this={assetViewerManager.imgRef}
|
||||||
src={imageLoaderUrl}
|
src={imageLoaderUrl}
|
||||||
alt={$getAltText(toTimelineAsset(asset))}
|
alt={$getAltText(toTimelineAsset(asset))}
|
||||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||||
@@ -237,7 +233,7 @@
|
|||||||
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, assetViewerManager.zoomState, assetViewerManager.imgRef) as boundingbox}
|
||||||
<div
|
<div
|
||||||
class="absolute border-solid border-white border-3 rounded-lg"
|
class="absolute border-solid border-white border-3 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;"
|
||||||
@@ -250,7 +246,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isFaceEditMode.value}
|
{#if isFaceEditMode.value}
|
||||||
<FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} />
|
<FaceEditor htmlElement={assetViewerManager.imgRef} {containerWidth} {containerHeight} assetId={asset.id} />
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
import SearchPeople from '$lib/components/faces-page/people-search.svelte';
|
||||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
import { zoomImageToBase64 } from '$lib/utils/people-utils';
|
||||||
@@ -61,7 +61,7 @@
|
|||||||
const handleCreatePerson = async () => {
|
const handleCreatePerson = async () => {
|
||||||
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
|
const timeout = setTimeout(() => (isShowLoadingNewPerson = true), timeBeforeShowLoadingSpinner);
|
||||||
|
|
||||||
const newFeaturePhoto = await zoomImageToBase64(editedFace, assetId, assetType, $photoViewerImgElement);
|
const newFeaturePhoto = await zoomImageToBase64(editedFace, assetId, assetType, assetViewerManager.imgRef);
|
||||||
|
|
||||||
onCreatePerson(newFeaturePhoto);
|
onCreatePerson(newFeaturePhoto);
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import OnEvents from '$lib/components/OnEvents.svelte';
|
import OnEvents from '$lib/components/OnEvents.svelte';
|
||||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
@@ -25,6 +24,7 @@
|
|||||||
import { fly } from 'svelte/transition';
|
import { fly } from 'svelte/transition';
|
||||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||||
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
||||||
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
@@ -269,7 +269,7 @@
|
|||||||
hidden={face.person.isHidden}
|
hidden={face.person.isHidden}
|
||||||
/>
|
/>
|
||||||
{:else}
|
{:else}
|
||||||
{#await zoomImageToBase64(face, assetId, assetType, $photoViewerImgElement)}
|
{#await zoomImageToBase64(face, assetId, assetType, assetViewerManager.imgRef)}
|
||||||
<ImageThumbnail
|
<ImageThumbnail
|
||||||
curve
|
curve
|
||||||
shadow
|
shadow
|
||||||
|
|||||||
@@ -1,8 +1,26 @@
|
|||||||
|
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
|
import { BaseEventManager } from '$lib/utils/base-event-manager.svelte';
|
||||||
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
import { PersistedLocalStorage } from '$lib/utils/persisted';
|
||||||
|
import type { ZoomImageWheelState } from '@zoom-image/core';
|
||||||
|
|
||||||
const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state', false);
|
const isShowDetailPanel = new PersistedLocalStorage<boolean>('asset-viewer-state', false);
|
||||||
|
|
||||||
export class AssetViewerManager {
|
export type Events = {
|
||||||
|
Zoom: [];
|
||||||
|
ZoomChange: [ZoomImageWheelState];
|
||||||
|
Copy: [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AssetViewerManager extends BaseEventManager<Events> {
|
||||||
|
#zoomState = $state<ZoomImageWheelState>({
|
||||||
|
currentRotation: 0,
|
||||||
|
currentZoom: 1,
|
||||||
|
enable: true,
|
||||||
|
currentPositionX: 0,
|
||||||
|
currentPositionY: 0,
|
||||||
|
});
|
||||||
|
|
||||||
|
imgRef = $state<HTMLImageElement | undefined>();
|
||||||
isShowActivityPanel = $state(false);
|
isShowActivityPanel = $state(false);
|
||||||
isPlayingMotionPhoto = $state(false);
|
isPlayingMotionPhoto = $state(false);
|
||||||
isShowEditor = $state(false);
|
isShowEditor = $state(false);
|
||||||
@@ -11,10 +29,44 @@ export class AssetViewerManager {
|
|||||||
return isShowDetailPanel.current;
|
return isShowDetailPanel.current;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get zoomState() {
|
||||||
|
return this.#zoomState;
|
||||||
|
}
|
||||||
|
|
||||||
|
set zoomState(state: ZoomImageWheelState) {
|
||||||
|
this.#zoomState = state;
|
||||||
|
this.emit('ZoomChange', state);
|
||||||
|
}
|
||||||
|
|
||||||
|
get zoom() {
|
||||||
|
return this.#zoomState.currentZoom;
|
||||||
|
}
|
||||||
|
|
||||||
|
set zoom(zoom: number) {
|
||||||
|
this.zoomState = { ...this.zoomState, currentZoom: zoom };
|
||||||
|
}
|
||||||
|
|
||||||
|
canZoomIn() {
|
||||||
|
return this.hasListeners('Zoom') && this.zoom <= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
canZoomOut() {
|
||||||
|
return this.hasListeners('Zoom') && this.zoom > 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
canCopyImage() {
|
||||||
|
return canCopyImageToClipboard() && !!assetViewerManager.imgRef;
|
||||||
|
}
|
||||||
|
|
||||||
private set isShowDetailPanel(value: boolean) {
|
private set isShowDetailPanel(value: boolean) {
|
||||||
isShowDetailPanel.current = value;
|
isShowDetailPanel.current = value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onZoomChange(state: ZoomImageWheelState) {
|
||||||
|
// bypass event emitter to avoid loop
|
||||||
|
this.#zoomState = state;
|
||||||
|
}
|
||||||
|
|
||||||
toggleActivityPanel() {
|
toggleActivityPanel() {
|
||||||
this.closeDetailPanel();
|
this.closeDetailPanel();
|
||||||
this.isShowActivityPanel = !this.isShowActivityPanel;
|
this.isShowActivityPanel = !this.isShowActivityPanel;
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import { modalManager, toastManager, type ActionItem } from '@immich/ui';
|
|||||||
import {
|
import {
|
||||||
mdiAlertOutline,
|
mdiAlertOutline,
|
||||||
mdiCogRefreshOutline,
|
mdiCogRefreshOutline,
|
||||||
|
mdiContentCopy,
|
||||||
mdiDatabaseRefreshOutline,
|
mdiDatabaseRefreshOutline,
|
||||||
mdiDownload,
|
mdiDownload,
|
||||||
mdiDownloadBox,
|
mdiDownloadBox,
|
||||||
@@ -35,6 +36,8 @@ import {
|
|||||||
mdiHeartOutline,
|
mdiHeartOutline,
|
||||||
mdiImageRefreshOutline,
|
mdiImageRefreshOutline,
|
||||||
mdiInformationOutline,
|
mdiInformationOutline,
|
||||||
|
mdiMagnifyMinusOutline,
|
||||||
|
mdiMagnifyPlusOutline,
|
||||||
mdiMotionPauseOutline,
|
mdiMotionPauseOutline,
|
||||||
mdiMotionPlayOutline,
|
mdiMotionPlayOutline,
|
||||||
mdiShareVariantOutline,
|
mdiShareVariantOutline,
|
||||||
@@ -125,6 +128,27 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
|||||||
onAction: () => assetViewerManager.toggleDetailPanel(),
|
onAction: () => assetViewerManager.toggleDetailPanel(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ZoomIn: ActionItem = {
|
||||||
|
title: $t('zoom_image'),
|
||||||
|
icon: mdiMagnifyPlusOutline,
|
||||||
|
$if: () => assetViewerManager.canZoomIn(),
|
||||||
|
onAction: () => assetViewerManager.emit('Zoom'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const ZoomOut: ActionItem = {
|
||||||
|
title: $t('zoom_image'),
|
||||||
|
icon: mdiMagnifyMinusOutline,
|
||||||
|
$if: () => assetViewerManager.canZoomOut(),
|
||||||
|
onAction: () => assetViewerManager.emit('Zoom'),
|
||||||
|
};
|
||||||
|
|
||||||
|
const Copy: ActionItem = {
|
||||||
|
title: $t('copy_image'),
|
||||||
|
icon: mdiContentCopy,
|
||||||
|
$if: () => assetViewerManager.canCopyImage(),
|
||||||
|
onAction: () => assetViewerManager.emit('Copy'),
|
||||||
|
};
|
||||||
|
|
||||||
const Info: ActionItem = {
|
const Info: ActionItem = {
|
||||||
title: $t('info'),
|
title: $t('info'),
|
||||||
icon: mdiInformationOutline,
|
icon: mdiInformationOutline,
|
||||||
@@ -185,6 +209,9 @@ export const getAssetActions = ($t: MessageFormatter, asset: AssetResponseDto) =
|
|||||||
Unfavorite,
|
Unfavorite,
|
||||||
PlayMotionPhoto,
|
PlayMotionPhoto,
|
||||||
StopMotionPhoto,
|
StopMotionPhoto,
|
||||||
|
ZoomIn,
|
||||||
|
ZoomOut,
|
||||||
|
Copy,
|
||||||
Edit,
|
Edit,
|
||||||
RefreshFacesJob,
|
RefreshFacesJob,
|
||||||
RefreshMetadataJob,
|
RefreshMetadataJob,
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { writable } from 'svelte/store';
|
import { writable } from 'svelte/store';
|
||||||
|
|
||||||
export const photoViewerImgElement = writable<HTMLImageElement | null>(null);
|
|
||||||
export const isSelectingAllAssets = writable(false);
|
export const isSelectingAllAssets = writable(false);
|
||||||
|
|||||||
@@ -1,25 +0,0 @@
|
|||||||
import type { ZoomImageWheelState } from '@zoom-image/core';
|
|
||||||
import { derived, writable } from 'svelte/store';
|
|
||||||
|
|
||||||
export const photoZoomState = writable<ZoomImageWheelState>({
|
|
||||||
currentRotation: 0,
|
|
||||||
currentZoom: 1,
|
|
||||||
enable: true,
|
|
||||||
currentPositionX: 0,
|
|
||||||
currentPositionY: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
export const photoZoomTransform = derived(
|
|
||||||
photoZoomState,
|
|
||||||
($state) => `translate(${$state.currentPositionX}px,${$state.currentPositionY}px) scale(${$state.currentZoom})`,
|
|
||||||
);
|
|
||||||
|
|
||||||
export const resetZoomState = () => {
|
|
||||||
photoZoomState.set({
|
|
||||||
currentRotation: 0,
|
|
||||||
currentZoom: 1,
|
|
||||||
enable: true,
|
|
||||||
currentPositionX: 0,
|
|
||||||
currentPositionY: 0,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
type BaseEvents = Record<string, unknown[]>;
|
||||||
|
|
||||||
|
export type EventCallback<Events extends BaseEvents, T extends keyof Events> = (
|
||||||
|
...args: Events[T]
|
||||||
|
) => Promise<void> | void;
|
||||||
|
export type EventItem<Events extends BaseEvents, T extends keyof Events = keyof Events> = {
|
||||||
|
id: number;
|
||||||
|
event: T;
|
||||||
|
callback: EventCallback<Events, T>;
|
||||||
|
};
|
||||||
|
|
||||||
|
let count = 1;
|
||||||
|
const nextId = () => count++;
|
||||||
|
|
||||||
|
const noop = () => {};
|
||||||
|
|
||||||
|
export class BaseEventManager<Events extends BaseEvents> {
|
||||||
|
#callbacks: EventItem<Events>[] = $state([]);
|
||||||
|
|
||||||
|
on<T extends keyof Events>(event: T, callback?: EventCallback<Events, T>) {
|
||||||
|
if (!callback) {
|
||||||
|
return noop;
|
||||||
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
const item = { id: nextId(), event, callback } as EventItem<Events, any>;
|
||||||
|
this.#callbacks.push(item);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
this.#callbacks = this.#callbacks.filter((current) => current.id !== item.id);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
emit<T extends keyof Events>(event: T, ...params: Events[T]) {
|
||||||
|
const listeners = this.getListeners(event);
|
||||||
|
for (const listener of listeners) {
|
||||||
|
void listener(...params);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
hasListeners<T extends keyof Events>(event: T) {
|
||||||
|
return this.#callbacks.some((item) => item.event === event);
|
||||||
|
}
|
||||||
|
|
||||||
|
private getListeners<T extends keyof Events>(event: T) {
|
||||||
|
return this.#callbacks
|
||||||
|
.filter((item) => item.event === event)
|
||||||
|
.map((item) => item.callback as EventCallback<Events, T>);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,9 +76,9 @@ export const zoomImageToBase64 = async (
|
|||||||
face: AssetFaceResponseDto,
|
face: AssetFaceResponseDto,
|
||||||
assetId: string,
|
assetId: string,
|
||||||
assetType: AssetTypeEnum,
|
assetType: AssetTypeEnum,
|
||||||
photoViewer: HTMLImageElement | null,
|
photoViewer: HTMLImageElement | undefined,
|
||||||
): Promise<string | null> => {
|
): Promise<string | null> => {
|
||||||
let image: HTMLImageElement | null = null;
|
let image: HTMLImageElement | undefined;
|
||||||
if (assetType === AssetTypeEnum.Image) {
|
if (assetType === AssetTypeEnum.Image) {
|
||||||
image = photoViewer;
|
image = photoViewer;
|
||||||
} else if (assetType === AssetTypeEnum.Video) {
|
} else if (assetType === AssetTypeEnum.Video) {
|
||||||
|
|||||||
Reference in New Issue
Block a user