refactor: zoom support
This commit is contained in:
@@ -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">
|
||||
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 MapModal from '$lib/modals/MapModal.svelte';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import AlbumMap from '$lib/components/album-page/album-map.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 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
<script lang="ts">
|
||||
import { shortcut } from '$lib/actions/shortcut';
|
||||
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 { IconButton } from '@immich/ui';
|
||||
import { mdiFolderDownloadOutline } from '@mdi/js';
|
||||
|
||||
@@ -21,9 +21,8 @@
|
||||
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 { 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 { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { getAssetJobName, getSharedLink } from '$lib/utils';
|
||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||
@@ -93,7 +92,8 @@
|
||||
motionPhoto,
|
||||
}: Props = $props();
|
||||
|
||||
let asset = $derived(assetManager.asset);
|
||||
let asset = $derived(assetManager.asset!);
|
||||
let zoomImageState = $derived(assetManager.zoomImageState);
|
||||
|
||||
const sharedLink = getSharedLink();
|
||||
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||
@@ -143,7 +143,7 @@
|
||||
color="secondary"
|
||||
variant="ghost"
|
||||
shape="round"
|
||||
icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
|
||||
icon={zoomImageState && zoomImageState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
|
||||
aria-label={$t('zoom_image')}
|
||||
onclick={onZoomImage}
|
||||
/>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte';
|
||||
import { AssetAction, ProjectionType } 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 { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
||||
import { isShowDetail } from '$lib/stores/preferences.store';
|
||||
@@ -96,7 +96,7 @@
|
||||
|
||||
let shouldPlayMotionPhoto = $state(false);
|
||||
let sharedLink = getSharedLink();
|
||||
let enableDetailPanel = $derived(asset.hasMetadata);
|
||||
let enableDetailPanel = $derived(asset?.hasMetadata ?? false);
|
||||
let slideshowStateUnsubscribe: () => void;
|
||||
let shuffleSlideshowUnsubscribe: () => void;
|
||||
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
||||
@@ -442,8 +442,7 @@
|
||||
{#if asset.type === AssetTypeEnum.Image}
|
||||
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
||||
<VideoViewer
|
||||
assetId={asset.livePhotoVideoId}
|
||||
cacheKey={asset.thumbhash}
|
||||
{assetManager}
|
||||
projectionType={asset.exifInfo?.projectionType}
|
||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||
onPreviousAsset={() => navigateAsset('previous')}
|
||||
@@ -453,15 +452,14 @@
|
||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
||||
.toLowerCase()
|
||||
.endsWith('.insp'))}
|
||||
<ImagePanoramaViewer {asset} />
|
||||
<ImagePanoramaViewer {assetManager} />
|
||||
{:else if isShowEditor && selectedEditType === 'crop'}
|
||||
<CropArea {asset} />
|
||||
{:else}
|
||||
<PhotoViewer
|
||||
bind:zoomToggle
|
||||
bind:copyImage
|
||||
{asset}
|
||||
{preloadAssets}
|
||||
{assetManager}
|
||||
onPreviousAsset={() => navigateAsset('previous')}
|
||||
onNextAsset={() => navigateAsset('next')}
|
||||
{sharedLink}
|
||||
@@ -470,8 +468,7 @@
|
||||
{/if}
|
||||
{:else}
|
||||
<VideoViewer
|
||||
assetId={asset.id}
|
||||
cacheKey={asset.thumbhash}
|
||||
{assetManager}
|
||||
projectionType={asset.exifInfo?.projectionType}
|
||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||
onPreviousAsset={() => navigateAsset('previous')}
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
import { onDestroy, onMount, tick } from 'svelte';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
||||
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||
import {
|
||||
changedOriention,
|
||||
cropAspectRatio,
|
||||
@@ -13,7 +14,6 @@
|
||||
rotateDegrees,
|
||||
} from '$lib/stores/asset-editor.store';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { animateCropChange, recalculateCrop } from './crop-settings';
|
||||
import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store';
|
||||
import { draw } from './drawing';
|
||||
@@ -21,10 +21,10 @@
|
||||
import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
assetManager: AssetManager;
|
||||
}
|
||||
|
||||
let { asset }: Props = $props();
|
||||
let { assetManager = $bindable() }: Props = $props();
|
||||
|
||||
let img = $state<HTMLImageElement>();
|
||||
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
<script lang="ts">
|
||||
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||
import { getAssetOriginalUrl } from '$lib/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 { fade } from 'svelte/transition';
|
||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||
|
||||
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 data = await viewAsset({ id, size: AssetMediaSize.Preview, key: authManager.key });
|
||||
|
||||
@@ -1,26 +1,28 @@
|
||||
<script lang="ts">
|
||||
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 BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||
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 type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
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 { photoZoomState } from '$lib/stores/zoom-image.store';
|
||||
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { onDestroy, onMount } from 'svelte';
|
||||
import { type SharedLinkResponseDto } from '@immich/sdk';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { swipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { fade } from 'svelte/transition';
|
||||
@@ -28,8 +30,7 @@
|
||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||
|
||||
interface Props {
|
||||
asset: AssetResponseDto;
|
||||
preloadAssets?: TimelineAsset[] | undefined;
|
||||
assetManager: AssetManager;
|
||||
element?: HTMLDivElement | undefined;
|
||||
haveFadeTransition?: boolean;
|
||||
sharedLink?: SharedLinkResponseDto | undefined;
|
||||
@@ -40,8 +41,7 @@
|
||||
}
|
||||
|
||||
let {
|
||||
asset,
|
||||
preloadAssets = undefined,
|
||||
assetManager = $bindable(),
|
||||
element = $bindable(),
|
||||
haveFadeTransition = true,
|
||||
sharedLink = undefined,
|
||||
@@ -53,51 +53,25 @@
|
||||
|
||||
const { slideshowState, slideshowLook } = slideshowStore;
|
||||
|
||||
let assetFileUrl: string = $state('');
|
||||
let imageLoaded: boolean = $state(false);
|
||||
let originalImageLoaded: boolean = $state(false);
|
||||
let imageError: boolean = $state(false);
|
||||
let zoomImageState = $derived(assetManager.zoomImageState);
|
||||
|
||||
let loader = $state<HTMLImageElement>();
|
||||
|
||||
photoZoomState.set({
|
||||
currentRotation: 0,
|
||||
currentZoom: 1,
|
||||
enable: true,
|
||||
currentPositionX: 0,
|
||||
currentPositionY: 0,
|
||||
});
|
||||
zoomToggle = () => {
|
||||
if (zoomImageState) {
|
||||
zoomImageState.currentZoom = zoomImageState.currentZoom > 1 ? 1 : 2;
|
||||
}
|
||||
};
|
||||
|
||||
onDestroy(() => {
|
||||
$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 () => {
|
||||
if (!canCopyImageToClipboard()) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl);
|
||||
await copyImageToClipboard($photoViewerImgElement ?? assetManager.url!);
|
||||
notificationController.show({
|
||||
type: NotificationType.Info,
|
||||
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);
|
||||
|
||||
$effect(() => {
|
||||
if (isFaceEditMode.value && $photoZoomState.currentZoom > 1) {
|
||||
if (isFaceEditMode.value && zoomImageState && zoomImageState.currentZoom > 1) {
|
||||
zoomToggle();
|
||||
}
|
||||
});
|
||||
@@ -132,7 +99,7 @@
|
||||
};
|
||||
|
||||
const onSwipe = (event: SwipeCustomEvent) => {
|
||||
if ($photoZoomState.currentZoom > 1) {
|
||||
if (!zoomImageState || zoomImageState.currentZoom > 1) {
|
||||
return;
|
||||
}
|
||||
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(() => {
|
||||
if (assetFileUrl) {
|
||||
if (assetManager.url) {
|
||||
// this can't be in an async context with $effect
|
||||
void cast(assetFileUrl);
|
||||
void cast(assetManager.url);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -175,35 +131,10 @@
|
||||
}
|
||||
};
|
||||
|
||||
const onload = () => {
|
||||
imageLoaded = true;
|
||||
assetFileUrl = imageLoaderUrl;
|
||||
originalImageLoaded = targetImageSize === AssetMediaSize.Fullsize || targetImageSize === 'original';
|
||||
};
|
||||
|
||||
const onerror = () => {
|
||||
imageError = imageLoaded = true;
|
||||
};
|
||||
|
||||
$effect(() => {
|
||||
preload(targetImageSize, preloadAssets);
|
||||
onDestroy(() => {
|
||||
cancelImageLoad(assetManager);
|
||||
});
|
||||
|
||||
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 containerHeight = $state(0);
|
||||
</script>
|
||||
@@ -217,27 +148,32 @@
|
||||
{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false },
|
||||
]}
|
||||
/>
|
||||
{#if imageError}
|
||||
{#if assetManager.loadError}
|
||||
<div class="h-full w-full">
|
||||
<BrokenAsset class="text-xl h-full w-full" />
|
||||
</div>
|
||||
{/if}
|
||||
<!-- svelte-ignore a11y_missing_attribute -->
|
||||
<img bind:this={loader} style="display:none" src={imageLoaderUrl} aria-hidden="true" />
|
||||
<div
|
||||
bind:this={element}
|
||||
class="relative h-full select-none"
|
||||
bind:clientWidth={containerWidth}
|
||||
bind:clientHeight={containerHeight}
|
||||
>
|
||||
<img style="display:none" src={imageLoaderUrl} alt="" {onload} {onerror} />
|
||||
{#if !imageLoaded}
|
||||
<img
|
||||
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">
|
||||
<LoadingSpinner />
|
||||
</div>
|
||||
{:else if !imageError}
|
||||
{:else if !assetManager.loadError}
|
||||
<div
|
||||
use:zoomImageAction
|
||||
{@attach zoomImageAttachment(assetManager)}
|
||||
use:swipe={() => ({})}
|
||||
onswipe={onSwipe}
|
||||
class="h-full w-full"
|
||||
@@ -245,7 +181,7 @@
|
||||
>
|
||||
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
||||
<img
|
||||
src={assetFileUrl}
|
||||
src={assetManager.url}
|
||||
alt=""
|
||||
class="-z-1 absolute top-0 start-0 object-cover h-full w-full blur-lg"
|
||||
draggable="false"
|
||||
@@ -253,15 +189,15 @@
|
||||
{/if}
|
||||
<img
|
||||
bind:this={$photoViewerImgElement}
|
||||
src={assetFileUrl}
|
||||
alt={$getAltText(toTimelineAsset(asset))}
|
||||
src={assetManager.url}
|
||||
alt={$getAltText(toTimelineAsset(assetManager.asset!))}
|
||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||
? 'object-contain'
|
||||
: slideshowLookCssMapping[$slideshowLook]}"
|
||||
draggable="false"
|
||||
/>
|
||||
<!-- eslint-disable-next-line svelte/require-each-key -->
|
||||
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox}
|
||||
{#each getBoundingBox($boundingBoxesArray, zoomImageState!, $photoViewerImgElement) as boundingbox}
|
||||
<div
|
||||
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;"
|
||||
@@ -270,7 +206,12 @@
|
||||
</div>
|
||||
|
||||
{#if isFaceEditMode.value}
|
||||
<FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} />
|
||||
<FaceEditor
|
||||
htmlElement={$photoViewerImgElement}
|
||||
{containerWidth}
|
||||
{containerHeight}
|
||||
assetId={assetManager.asset!.id}
|
||||
/>
|
||||
{/if}
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
@@ -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 = () => {},
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
<script lang="ts">
|
||||
import { ProjectionType } from '$lib/constants';
|
||||
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-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 {
|
||||
assetId: string;
|
||||
assetManager: AssetManager;
|
||||
projectionType: string | null | undefined;
|
||||
cacheKey: string | null;
|
||||
loopVideo: boolean;
|
||||
onClose?: () => void;
|
||||
onPreviousAsset?: () => void;
|
||||
@@ -15,10 +15,10 @@
|
||||
onVideoStarted?: () => void;
|
||||
}
|
||||
|
||||
// TODO: do not preload assets.
|
||||
let {
|
||||
assetId,
|
||||
assetManager = $bindable(),
|
||||
projectionType,
|
||||
cacheKey,
|
||||
loopVideo,
|
||||
onPreviousAsset,
|
||||
onClose,
|
||||
@@ -29,12 +29,11 @@
|
||||
</script>
|
||||
|
||||
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||
<VideoPanoramaViewer {assetId} />
|
||||
<VideoPanoramaViewer {assetManager} />
|
||||
{:else}
|
||||
<VideoNativeViewer
|
||||
{loopVideo}
|
||||
{cacheKey}
|
||||
{assetId}
|
||||
{assetManager}
|
||||
{onPreviousAsset}
|
||||
{onNextAsset}
|
||||
{onVideoEnded}
|
||||
|
||||
@@ -26,7 +26,7 @@
|
||||
NotificationType,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
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 type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.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 { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
@@ -439,35 +439,38 @@
|
||||
};
|
||||
|
||||
const handlePrevious = async () => {
|
||||
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 });
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 });
|
||||
}
|
||||
}
|
||||
204
web/src/lib/managers/asset-manager/asset-manager.svelte.ts
Normal file
204
web/src/lib/managers/asset-manager/asset-manager.svelte.ts
Normal file
@@ -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();
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -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 { 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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 type { PageData } from './$types';
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
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 { 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetVisibility } from '@immich/sdk';
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
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 { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
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 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 { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||
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 type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||
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 PasswordField from '$lib/components/shared-components/password-field.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 { setSharedLink } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { onDestroy } from 'svelte';
|
||||
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';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
notificationController,
|
||||
} from '$lib/components/shared-components/notification/notification';
|
||||
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 { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
import { t } from 'svelte-i18n';
|
||||
import type { PageData } from './$types';
|
||||
import { onDestroy } from 'svelte';
|
||||
import { AssetManager } from '$lib/managers/asset-manager.svelte';
|
||||
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||
|
||||
interface Props {
|
||||
data: PageData;
|
||||
|
||||
Reference in New Issue
Block a user