mirror of
https://github.com/immich-app/immich.git
synced 2025-12-07 21:30:59 -08:00
Compare commits
3 Commits
prevent-so
...
refact/ass
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab988f3be6 | ||
|
|
b8dc1a4b1f | ||
|
|
769d0aed87 |
20
web/package-lock.json
generated
20
web/package-lock.json
generated
@@ -26,6 +26,7 @@
|
|||||||
"intl-messageformat": "^10.7.11",
|
"intl-messageformat": "^10.7.11",
|
||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"lru-cache": "^11.1.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"maplibre-gl": "^5.3.0",
|
"maplibre-gl": "^5.3.0",
|
||||||
"pmtiles": "^4.3.0",
|
"pmtiles": "^4.3.0",
|
||||||
@@ -6592,11 +6593,13 @@
|
|||||||
"license": "MIT"
|
"license": "MIT"
|
||||||
},
|
},
|
||||||
"node_modules/lru-cache": {
|
"node_modules/lru-cache": {
|
||||||
"version": "10.4.3",
|
"version": "11.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.1.0.tgz",
|
||||||
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
"integrity": "sha512-QIXZUBJUx+2zHUdQujWejBkcD9+cs94tLn0+YL8UrCh+D5sCXZ4c7LaEH48pNwRY3MLDgqUFyhlCyjJPf1WP0A==",
|
||||||
"dev": true,
|
"license": "ISC",
|
||||||
"license": "ISC"
|
"engines": {
|
||||||
|
"node": "20 || >=22"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"node_modules/lru-queue": {
|
"node_modules/lru-queue": {
|
||||||
"version": "0.1.0",
|
"version": "0.1.0",
|
||||||
@@ -7335,6 +7338,13 @@
|
|||||||
"url": "https://github.com/sponsors/isaacs"
|
"url": "https://github.com/sponsors/isaacs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/path-scurry/node_modules/lru-cache": {
|
||||||
|
"version": "10.4.3",
|
||||||
|
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz",
|
||||||
|
"integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "ISC"
|
||||||
|
},
|
||||||
"node_modules/pathe": {
|
"node_modules/pathe": {
|
||||||
"version": "2.0.3",
|
"version": "2.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz",
|
||||||
|
|||||||
@@ -43,6 +43,7 @@
|
|||||||
"intl-messageformat": "^10.7.11",
|
"intl-messageformat": "^10.7.11",
|
||||||
"justified-layout": "^4.1.0",
|
"justified-layout": "^4.1.0",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
|
"lru-cache": "^11.1.0",
|
||||||
"luxon": "^3.4.4",
|
"luxon": "^3.4.4",
|
||||||
"maplibre-gl": "^5.3.0",
|
"maplibre-gl": "^5.3.0",
|
||||||
"pmtiles": "^4.3.0",
|
"pmtiles": "^4.3.0",
|
||||||
|
|||||||
@@ -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,7 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import MapModal from '$lib/modals/MapModal.svelte';
|
import MapModal from '$lib/modals/MapModal.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
|
import { getAlbumInfo, type AlbumResponseDto, type MapMarkerResponseDto } from '@immich/sdk';
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiMapOutline } from '@mdi/js';
|
import { mdiMapOutline } from '@mdi/js';
|
||||||
@@ -9,12 +10,12 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
assetManager: AssetManager;
|
||||||
album: AlbumResponseDto;
|
album: AlbumResponseDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { album }: Props = $props();
|
let { assetManager = $bindable(), album }: Props = $props();
|
||||||
let abortController: AbortController;
|
let abortController: AbortController;
|
||||||
let { setAssetId } = assetViewingStore;
|
|
||||||
|
|
||||||
let mapMarkers: MapMarkerResponseDto[] = $state([]);
|
let mapMarkers: MapMarkerResponseDto[] = $state([]);
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@
|
|||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
abortController?.abort();
|
abortController?.abort();
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetManager.showAssetViewer = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
async function loadMapMarkers() {
|
async function loadMapMarkers() {
|
||||||
@@ -56,7 +57,7 @@
|
|||||||
const assetIds = await modalManager.show(MapModal, { mapMarkers });
|
const assetIds = await modalManager.show(MapModal, { mapMarkers });
|
||||||
|
|
||||||
if (assetIds) {
|
if (assetIds) {
|
||||||
await setAssetId(assetIds[0]);
|
await navigate({ targetRoute: 'current', assetId: assetIds[0] });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
import AlbumMap from '$lib/components/album-page/album-map.svelte';
|
import AlbumMap from '$lib/components/album-page/album-map.svelte';
|
||||||
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
|
||||||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
@@ -25,16 +25,15 @@
|
|||||||
import AlbumSummary from './album-summary.svelte';
|
import AlbumSummary from './album-summary.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
assetManager: AssetManager;
|
||||||
sharedLink: SharedLinkResponseDto;
|
sharedLink: SharedLinkResponseDto;
|
||||||
user?: UserResponseDto | undefined;
|
user?: UserResponseDto | undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { sharedLink, user = undefined }: Props = $props();
|
let { assetManager = $bindable(), sharedLink, user = undefined }: Props = $props();
|
||||||
|
|
||||||
const album = sharedLink.album as AlbumResponseDto;
|
const album = sharedLink.album as AlbumResponseDto;
|
||||||
|
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
|
||||||
|
|
||||||
const timelineManager = new TimelineManager();
|
const timelineManager = new TimelineManager();
|
||||||
$effect(() => void timelineManager.updateOptions({ albumId: album.id, order: album.order }));
|
$effect(() => void timelineManager.updateOptions({ albumId: album.id, order: album.order }));
|
||||||
onDestroy(() => timelineManager.destroy());
|
onDestroy(() => timelineManager.destroy());
|
||||||
@@ -53,7 +52,7 @@
|
|||||||
use:shortcut={{
|
use:shortcut={{
|
||||||
shortcut: { key: 'Escape' },
|
shortcut: { key: 'Escape' },
|
||||||
onShortcut: () => {
|
onShortcut: () => {
|
||||||
if (!$showAssetViewer && assetInteraction.selectionActive) {
|
if (!assetManager.showAssetViewer && assetInteraction.selectionActive) {
|
||||||
cancelMultiselect(assetInteraction);
|
cancelMultiselect(assetInteraction);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -61,7 +60,7 @@
|
|||||||
/>
|
/>
|
||||||
|
|
||||||
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
||||||
<AssetGrid enableRouting={true} {album} {timelineManager} {assetInteraction}>
|
<AssetGrid enableRouting={true} {album} {timelineManager} {assetInteraction} {assetManager}>
|
||||||
<section class="pt-8 md:pt-24 px-2 md:px-0">
|
<section class="pt-8 md:pt-24 px-2 md:px-0">
|
||||||
<!-- ALBUM TITLE -->
|
<!-- ALBUM TITLE -->
|
||||||
<h1
|
<h1
|
||||||
@@ -129,7 +128,7 @@
|
|||||||
/>
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{#if sharedLink.showMetadata && $featureFlags.loaded && $featureFlags.map}
|
{#if sharedLink.showMetadata && $featureFlags.loaded && $featureFlags.map}
|
||||||
<AlbumMap {album} />
|
<AlbumMap {assetManager} {album} />
|
||||||
{/if}
|
{/if}
|
||||||
<ThemeButton />
|
<ThemeButton />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -1,22 +1,20 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shortcut } from '$lib/actions/shortcut';
|
import { shortcut } from '$lib/actions/shortcut';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
||||||
import { downloadFile } from '$lib/utils/asset-utils';
|
import { downloadFile } from '$lib/utils/asset-utils';
|
||||||
import { getAssetInfo } from '@immich/sdk';
|
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiFolderDownloadOutline } from '@mdi/js';
|
import { mdiFolderDownloadOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: TimelineAsset;
|
assetManager: AssetManager;
|
||||||
menuItem?: boolean;
|
menuItem?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { asset, menuItem = false }: Props = $props();
|
let { assetManager = $bindable(), menuItem = false }: Props = $props();
|
||||||
|
|
||||||
const onDownloadFile = async () => downloadFile(await getAssetInfo({ id: asset.id, key: authManager.key }));
|
const onDownloadFile = async () => downloadFile(assetManager.asset);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />
|
<svelte:document use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
|
||||||
import { getAssetJobName, getSharedLink } from '$lib/utils';
|
import { getAssetJobName, getSharedLink } from '$lib/utils';
|
||||||
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
import { canCopyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
@@ -32,7 +32,6 @@
|
|||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
AssetVisibility,
|
AssetVisibility,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
|
||||||
type PersonResponseDto,
|
type PersonResponseDto,
|
||||||
type StackResponseDto,
|
type StackResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
@@ -55,7 +54,7 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
assetManager: AssetManager;
|
||||||
album?: AlbumResponseDto | null;
|
album?: AlbumResponseDto | null;
|
||||||
person?: PersonResponseDto | null;
|
person?: PersonResponseDto | null;
|
||||||
stack?: StackResponseDto | null;
|
stack?: StackResponseDto | null;
|
||||||
@@ -75,7 +74,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
asset,
|
assetManager = $bindable(),
|
||||||
album = null,
|
album = null,
|
||||||
person = null,
|
person = null,
|
||||||
stack = null,
|
stack = null,
|
||||||
@@ -93,6 +92,9 @@
|
|||||||
motionPhoto,
|
motionPhoto,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
|
let asset = $derived(assetManager.asset!);
|
||||||
|
let zoomImageState = $derived(assetManager.zoomImageState);
|
||||||
|
|
||||||
const sharedLink = getSharedLink();
|
const sharedLink = getSharedLink();
|
||||||
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
let isOwner = $derived($user && asset.ownerId === $user?.id);
|
||||||
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
|
||||||
@@ -141,7 +143,7 @@
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
variant="ghost"
|
variant="ghost"
|
||||||
shape="round"
|
shape="round"
|
||||||
icon={$photoZoomState && $photoZoomState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
|
icon={zoomImageState && zoomImageState.currentZoom > 1 ? mdiMagnifyMinusOutline : mdiMagnifyPlusOutline}
|
||||||
aria-label={$t('zoom_image')}
|
aria-label={$t('zoom_image')}
|
||||||
onclick={onZoomImage}
|
onclick={onZoomImage}
|
||||||
/>
|
/>
|
||||||
@@ -158,7 +160,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isOwner && showDownloadButton}
|
{#if !isOwner && showDownloadButton}
|
||||||
<DownloadAction asset={toTimelineAsset(asset)} />
|
<DownloadAction {assetManager} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if showDetailButton}
|
{#if showDetailButton}
|
||||||
@@ -177,7 +179,7 @@
|
|||||||
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if showDownloadButton}
|
{#if showDownloadButton}
|
||||||
<DownloadAction asset={toTimelineAsset(asset)} menuItem />
|
<DownloadAction {assetManager} menuItem />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !isLocked}
|
{#if !isLocked}
|
||||||
|
|||||||
@@ -7,22 +7,21 @@
|
|||||||
import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte';
|
import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte';
|
||||||
import { AssetAction, ProjectionType } from '$lib/constants';
|
import { AssetAction, ProjectionType } from '$lib/constants';
|
||||||
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
||||||
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
import { closeEditorCofirm } from '$lib/stores/asset-editor.store';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { isShowDetail } from '$lib/stores/preferences.store';
|
import { isShowDetail } from '$lib/stores/preferences.store';
|
||||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils';
|
import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
import { SlideshowHistory } from '$lib/utils/slideshow-history';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import {
|
import {
|
||||||
AssetJobName,
|
AssetJobName,
|
||||||
AssetTypeEnum,
|
AssetTypeEnum,
|
||||||
getAllAlbums,
|
|
||||||
getStack,
|
getStack,
|
||||||
runAssetJobs,
|
runAssetJobs,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
@@ -48,8 +47,7 @@
|
|||||||
type HasAsset = boolean;
|
type HasAsset = boolean;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
assetManager: AssetManager;
|
||||||
preloadAssets?: TimelineAsset[];
|
|
||||||
showNavigation?: boolean;
|
showNavigation?: boolean;
|
||||||
withStacked?: boolean;
|
withStacked?: boolean;
|
||||||
isShared?: boolean;
|
isShared?: boolean;
|
||||||
@@ -61,13 +59,12 @@
|
|||||||
onClose: (asset: AssetResponseDto) => void;
|
onClose: (asset: AssetResponseDto) => void;
|
||||||
onNext: () => Promise<HasAsset>;
|
onNext: () => Promise<HasAsset>;
|
||||||
onPrevious: () => Promise<HasAsset>;
|
onPrevious: () => Promise<HasAsset>;
|
||||||
onRandom: () => Promise<{ id: string } | undefined>;
|
onRandom: () => Promise<HasAsset>;
|
||||||
copyImage?: () => Promise<void>;
|
copyImage?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
asset = $bindable(),
|
assetManager = $bindable(),
|
||||||
preloadAssets = $bindable([]),
|
|
||||||
showNavigation = true,
|
showNavigation = true,
|
||||||
withStacked = false,
|
withStacked = false,
|
||||||
isShared = false,
|
isShared = false,
|
||||||
@@ -83,7 +80,6 @@
|
|||||||
copyImage = $bindable(),
|
copyImage = $bindable(),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const { setAssetId } = assetViewingStore;
|
|
||||||
const {
|
const {
|
||||||
restartProgress: restartSlideshowProgress,
|
restartProgress: restartSlideshowProgress,
|
||||||
stopProgress: stopSlideshowProgress,
|
stopProgress: stopSlideshowProgress,
|
||||||
@@ -94,10 +90,13 @@
|
|||||||
const stackThumbnailSize = 60;
|
const stackThumbnailSize = 60;
|
||||||
const stackSelectedThumbnailSize = 65;
|
const stackSelectedThumbnailSize = 65;
|
||||||
|
|
||||||
let appearsInAlbums: AlbumResponseDto[] = $state([]);
|
let asset = $derived(assetManager.asset);
|
||||||
|
let preloadAssets = $derived(assetManager.preloadAssets);
|
||||||
|
let albums = $derived(assetManager.albums);
|
||||||
|
|
||||||
let shouldPlayMotionPhoto = $state(false);
|
let shouldPlayMotionPhoto = $state(false);
|
||||||
let sharedLink = getSharedLink();
|
let sharedLink = getSharedLink();
|
||||||
let enableDetailPanel = asset.hasMetadata;
|
let enableDetailPanel = $derived(asset?.hasMetadata ?? false);
|
||||||
let slideshowStateUnsubscribe: () => void;
|
let slideshowStateUnsubscribe: () => void;
|
||||||
let shuffleSlideshowUnsubscribe: () => void;
|
let shuffleSlideshowUnsubscribe: () => void;
|
||||||
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
let previewStackedAsset: AssetResponseDto | undefined = $state();
|
||||||
@@ -106,7 +105,6 @@
|
|||||||
let fullscreenElement = $state<Element>();
|
let fullscreenElement = $state<Element>();
|
||||||
let unsubscribes: (() => void)[] = [];
|
let unsubscribes: (() => void)[] = [];
|
||||||
let selectedEditType: string = $state('');
|
let selectedEditType: string = $state('');
|
||||||
let stack: StackResponseDto | null = $state(null);
|
|
||||||
|
|
||||||
let zoomToggle = $state(() => void 0);
|
let zoomToggle = $state(() => void 0);
|
||||||
|
|
||||||
@@ -146,7 +144,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(() => {
|
||||||
unsubscribes.push(
|
unsubscribes.push(
|
||||||
websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })),
|
websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })),
|
||||||
websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })),
|
websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })),
|
||||||
@@ -169,9 +167,7 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!sharedLink) {
|
// TODO: empty shared link returns 404.
|
||||||
await handleGetAllAlbums();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
@@ -190,18 +186,6 @@
|
|||||||
activityManager.reset();
|
activityManager.reset();
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleGetAllAlbums = async () => {
|
|
||||||
if (authManager.key) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
appearsInAlbums = await getAllAlbums({ assetId: asset.id });
|
|
||||||
} catch (error) {
|
|
||||||
console.error('Error getting album that asset belong to', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleOpenActivity = () => {
|
const handleOpenActivity = () => {
|
||||||
if ($isShowDetail) {
|
if ($isShowDetail) {
|
||||||
$isShowDetail = false;
|
$isShowDetail = false;
|
||||||
@@ -238,11 +222,11 @@
|
|||||||
let hasNext = false;
|
let hasNext = false;
|
||||||
|
|
||||||
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
if ($slideshowState === SlideshowState.PlaySlideshow && $slideshowNavigation === SlideshowNavigation.Shuffle) {
|
||||||
hasNext = order === 'previous' ? slideshowHistory.previous() : slideshowHistory.next();
|
hasNext = order === 'previous' ? await slideshowHistory.previous() : await slideshowHistory.next();
|
||||||
if (!hasNext) {
|
if (!hasNext) {
|
||||||
const asset = await onRandom();
|
await onRandom();
|
||||||
if (asset) {
|
if (assetManager.asset) {
|
||||||
slideshowHistory.queue(asset);
|
slideshowHistory.queue(assetManager.asset);
|
||||||
hasNext = true;
|
hasNext = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -281,8 +265,9 @@
|
|||||||
|
|
||||||
let assetViewerHtmlElement = $state<HTMLElement>();
|
let assetViewerHtmlElement = $state<HTMLElement>();
|
||||||
|
|
||||||
const slideshowHistory = new SlideshowHistory((asset) => {
|
const slideshowHistory = new SlideshowHistory(async (asset) => {
|
||||||
handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true)));
|
await navigate({ targetRoute: 'current', assetId: asset.id });
|
||||||
|
$restartSlideshowProgress = true;
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVideoStarted = () => {
|
const handleVideoStarted = () => {
|
||||||
@@ -325,7 +310,7 @@
|
|||||||
const handleAction = async (action: Action) => {
|
const handleAction = async (action: Action) => {
|
||||||
switch (action.type) {
|
switch (action.type) {
|
||||||
case AssetAction.ADD_TO_ALBUM: {
|
case AssetAction.ADD_TO_ALBUM: {
|
||||||
await handleGetAllAlbums();
|
await assetManager.refreshAlbums();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case AssetAction.SET_STACK_PRIMARY_ASSET: {
|
case AssetAction.SET_STACK_PRIMARY_ASSET: {
|
||||||
@@ -364,11 +349,6 @@
|
|||||||
handlePromiseError(activityManager.init(album.id, asset.id));
|
handlePromiseError(activityManager.init(album.id, asset.id));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
$effect(() => {
|
|
||||||
if (asset.id) {
|
|
||||||
handlePromiseError(handleGetAllAlbums());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document bind:fullscreenElement />
|
<svelte:document bind:fullscreenElement />
|
||||||
@@ -383,7 +363,7 @@
|
|||||||
{#if $slideshowState === SlideshowState.None && !isShowEditor}
|
{#if $slideshowState === SlideshowState.None && !isShowEditor}
|
||||||
<div class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform">
|
<div class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform">
|
||||||
<AssetViewerNavBar
|
<AssetViewerNavBar
|
||||||
{asset}
|
{assetManager}
|
||||||
{album}
|
{album}
|
||||||
{person}
|
{person}
|
||||||
{stack}
|
{stack}
|
||||||
@@ -461,8 +441,7 @@
|
|||||||
{#if asset.type === AssetTypeEnum.Image}
|
{#if asset.type === AssetTypeEnum.Image}
|
||||||
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
{#if shouldPlayMotionPhoto && asset.livePhotoVideoId}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={asset.livePhotoVideoId}
|
{assetManager}
|
||||||
cacheKey={asset.thumbhash}
|
|
||||||
projectionType={asset.exifInfo?.projectionType}
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
@@ -472,15 +451,14 @@
|
|||||||
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
{:else if asset.exifInfo?.projectionType === ProjectionType.EQUIRECTANGULAR || (asset.originalPath && asset.originalPath
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.endsWith('.insp'))}
|
.endsWith('.insp'))}
|
||||||
<ImagePanoramaViewer {asset} />
|
<ImagePanoramaViewer {assetManager} />
|
||||||
{:else if isShowEditor && selectedEditType === 'crop'}
|
{:else if isShowEditor && selectedEditType === 'crop'}
|
||||||
<CropArea {asset} />
|
<CropArea {asset} />
|
||||||
{:else}
|
{:else}
|
||||||
<PhotoViewer
|
<PhotoViewer
|
||||||
bind:zoomToggle
|
bind:zoomToggle
|
||||||
bind:copyImage
|
bind:copyImage
|
||||||
{asset}
|
{assetManager}
|
||||||
{preloadAssets}
|
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
onNextAsset={() => navigateAsset('next')}
|
onNextAsset={() => navigateAsset('next')}
|
||||||
{sharedLink}
|
{sharedLink}
|
||||||
@@ -489,8 +467,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{:else}
|
{:else}
|
||||||
<VideoViewer
|
<VideoViewer
|
||||||
assetId={asset.id}
|
{assetManager}
|
||||||
cacheKey={asset.thumbhash}
|
|
||||||
projectionType={asset.exifInfo?.projectionType}
|
projectionType={asset.exifInfo?.projectionType}
|
||||||
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
loopVideo={$slideshowState !== SlideshowState.PlaySlideshow}
|
||||||
onPreviousAsset={() => navigateAsset('previous')}
|
onPreviousAsset={() => navigateAsset('previous')}
|
||||||
@@ -529,7 +506,7 @@
|
|||||||
class="row-start-1 row-span-4 w-[360px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light"
|
class="row-start-1 row-span-4 w-[360px] overflow-y-auto transition-all dark:border-l dark:border-s-immich-dark-gray bg-light"
|
||||||
translate="yes"
|
translate="yes"
|
||||||
>
|
>
|
||||||
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
|
<DetailPanel {asset} currentAlbum={album} {albums} onClose={() => ($isShowDetail = false)} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@
|
|||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import { getAssetThumbnailUrl, getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { getAssetThumbnailUrl, getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { delay, isFlipped } from '$lib/utils/asset-utils';
|
import { delay, isFlipped } from '$lib/utils/asset-utils';
|
||||||
import { getByteUnitString } from '$lib/utils/byte-units';
|
import { getByteUnitString } from '$lib/utils/byte-units';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
@@ -22,7 +22,6 @@
|
|||||||
import { getParentPath } from '$lib/utils/tree-utils';
|
import { getParentPath } from '$lib/utils/tree-utils';
|
||||||
import {
|
import {
|
||||||
AssetMediaSize,
|
AssetMediaSize,
|
||||||
getAssetInfo,
|
|
||||||
updateAsset,
|
updateAsset,
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
type AssetResponseDto,
|
||||||
@@ -83,19 +82,6 @@
|
|||||||
|
|
||||||
let isOwner = $derived($user?.id === asset.ownerId);
|
let isOwner = $derived($user?.id === asset.ownerId);
|
||||||
|
|
||||||
const handleNewAsset = async (newAsset: AssetResponseDto) => {
|
|
||||||
// TODO: check if reloading asset data is necessary
|
|
||||||
if (newAsset.id && !authManager.key) {
|
|
||||||
const data = await getAssetInfo({ id: asset.id });
|
|
||||||
people = data?.people || [];
|
|
||||||
unassignedFaces = data?.unassignedFaces || [];
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
handlePromiseError(handleNewAsset(asset));
|
|
||||||
});
|
|
||||||
|
|
||||||
let latlng = $derived(
|
let latlng = $derived(
|
||||||
(() => {
|
(() => {
|
||||||
const lat = asset.exifInfo?.latitude;
|
const lat = asset.exifInfo?.latitude;
|
||||||
@@ -127,11 +113,8 @@
|
|||||||
return undefined;
|
return undefined;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRefreshPeople = async () => {
|
// TODO: refresh people
|
||||||
await getAssetInfo({ id: asset.id }).then((data) => {
|
const handleRefreshPeople = () => {
|
||||||
people = data?.people || [];
|
|
||||||
unassignedFaces = data?.unassignedFaces || [];
|
|
||||||
});
|
|
||||||
showEditFaces = false;
|
showEditFaces = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@
|
|||||||
import { onDestroy, onMount, tick } from 'svelte';
|
import { onDestroy, onMount, tick } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import {
|
import {
|
||||||
changedOriention,
|
changedOriention,
|
||||||
cropAspectRatio,
|
cropAspectRatio,
|
||||||
@@ -13,7 +14,6 @@
|
|||||||
rotateDegrees,
|
rotateDegrees,
|
||||||
} from '$lib/stores/asset-editor.store';
|
} from '$lib/stores/asset-editor.store';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import type { AssetResponseDto } from '@immich/sdk';
|
|
||||||
import { animateCropChange, recalculateCrop } from './crop-settings';
|
import { animateCropChange, recalculateCrop } from './crop-settings';
|
||||||
import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store';
|
import { cropAreaEl, cropFrame, imgElement, isResizingOrDragging, overlayEl, resetCropStore } from './crop-store';
|
||||||
import { draw } from './drawing';
|
import { draw } from './drawing';
|
||||||
@@ -21,10 +21,10 @@
|
|||||||
import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers';
|
import { handleMouseDown, handleMouseMove, handleMouseUp } from './mouse-handlers';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
assetManager: AssetManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { asset }: Props = $props();
|
let { assetManager = $bindable() }: Props = $props();
|
||||||
|
|
||||||
let img = $state<HTMLImageElement>();
|
let img = $state<HTMLImageElement>();
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
|
||||||
import { notificationController } from '$lib/components/shared-components/notification/notification';
|
import { notificationController } from '$lib/components/shared-components/notification/notification';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
@@ -303,7 +302,8 @@
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
await assetViewingStore.setAssetId(assetId);
|
// TODO: manual tag face
|
||||||
|
// await assetViewingStore.setAssetId(assetId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, 'Error tagging face');
|
handleError(error, 'Error tagging face');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,17 +1,19 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { getAssetOriginalUrl } from '$lib/utils';
|
import { getAssetOriginalUrl } from '$lib/utils';
|
||||||
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
import { isWebCompatibleImage } from '$lib/utils/asset-utils';
|
||||||
import { AssetMediaSize, viewAsset, type AssetResponseDto } from '@immich/sdk';
|
import { AssetMediaSize, viewAsset } from '@immich/sdk';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
assetManager: AssetManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { asset }: Props = $props();
|
// TODO: do not preload assets.
|
||||||
|
const { assetManager = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const loadAssetData = async (id: string) => {
|
const loadAssetData = async (id: string) => {
|
||||||
const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: authManager.key });
|
const data = await viewAsset({ id, size: AssetMediaSize.Preview, key: authManager.key });
|
||||||
|
|||||||
@@ -1,26 +1,28 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
import { zoomImageAction } from '$lib/actions/zoom-image';
|
|
||||||
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
|
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
|
||||||
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
import BrokenAsset from '$lib/components/assets/broken-asset.svelte';
|
||||||
import { assetViewerFadeDuration } from '$lib/constants';
|
import { assetViewerFadeDuration } from '$lib/constants';
|
||||||
|
import { type AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
import {
|
||||||
|
cancelImageLoad,
|
||||||
|
mediaLoadError,
|
||||||
|
mediaLoaded,
|
||||||
|
} from '$lib/managers/asset-manager/internal/load-support.svelte';
|
||||||
|
import { zoomImageAttachment } from '$lib/managers/asset-manager/internal/zoom-support.svelte';
|
||||||
import { castManager } from '$lib/managers/cast-manager.svelte';
|
import { castManager } from '$lib/managers/cast-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
||||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { alwaysLoadOriginalFile } from '$lib/stores/preferences.store';
|
|
||||||
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowLook, SlideshowState, slideshowLookCssMapping, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { photoZoomState } from '$lib/stores/zoom-image.store';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { getAssetOriginalUrl, getAssetThumbnailUrl, handlePromiseError } from '$lib/utils';
|
import { canCopyImageToClipboard, copyImageToClipboard } from '$lib/utils/asset-utils';
|
||||||
import { canCopyImageToClipboard, copyImageToClipboard, isWebCompatibleImage } from '$lib/utils/asset-utils';
|
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { getBoundingBox } from '$lib/utils/people-utils';
|
import { getBoundingBox } from '$lib/utils/people-utils';
|
||||||
import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
|
||||||
import { getAltText } from '$lib/utils/thumbnail-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { AssetMediaSize, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
|
import { type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
import { onDestroy, onMount } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { swipe, type SwipeCustomEvent } from 'svelte-gestures';
|
import { swipe, type SwipeCustomEvent } from 'svelte-gestures';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
@@ -28,8 +30,7 @@
|
|||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
asset: AssetResponseDto;
|
assetManager: AssetManager;
|
||||||
preloadAssets?: TimelineAsset[] | undefined;
|
|
||||||
element?: HTMLDivElement | undefined;
|
element?: HTMLDivElement | undefined;
|
||||||
haveFadeTransition?: boolean;
|
haveFadeTransition?: boolean;
|
||||||
sharedLink?: SharedLinkResponseDto | undefined;
|
sharedLink?: SharedLinkResponseDto | undefined;
|
||||||
@@ -40,8 +41,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
asset,
|
assetManager = $bindable(),
|
||||||
preloadAssets = undefined,
|
|
||||||
element = $bindable(),
|
element = $bindable(),
|
||||||
haveFadeTransition = true,
|
haveFadeTransition = true,
|
||||||
sharedLink = undefined,
|
sharedLink = undefined,
|
||||||
@@ -53,51 +53,25 @@
|
|||||||
|
|
||||||
const { slideshowState, slideshowLook } = slideshowStore;
|
const { slideshowState, slideshowLook } = slideshowStore;
|
||||||
|
|
||||||
let assetFileUrl: string = $state('');
|
let zoomImageState = $derived(assetManager.zoomImageState);
|
||||||
let imageLoaded: boolean = $state(false);
|
|
||||||
let originalImageLoaded: boolean = $state(false);
|
|
||||||
let imageError: boolean = $state(false);
|
|
||||||
|
|
||||||
let loader = $state<HTMLImageElement>();
|
zoomToggle = () => {
|
||||||
|
if (zoomImageState) {
|
||||||
photoZoomState.set({
|
zoomImageState.currentZoom = zoomImageState.currentZoom > 1 ? 1 : 2;
|
||||||
currentRotation: 0,
|
}
|
||||||
currentZoom: 1,
|
};
|
||||||
enable: true,
|
|
||||||
currentPositionX: 0,
|
|
||||||
currentPositionY: 0,
|
|
||||||
});
|
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
$boundingBoxesArray = [];
|
$boundingBoxesArray = [];
|
||||||
});
|
});
|
||||||
|
|
||||||
const preload = (targetSize: AssetMediaSize | 'original', preloadAssets?: TimelineAsset[]) => {
|
|
||||||
for (const preloadAsset of preloadAssets || []) {
|
|
||||||
if (preloadAsset.isImage) {
|
|
||||||
let img = new Image();
|
|
||||||
img.src = getAssetUrl(preloadAsset.id, targetSize, preloadAsset.thumbhash);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAssetUrl = (id: string, targetSize: AssetMediaSize | 'original', cacheKey: string | null) => {
|
|
||||||
if (sharedLink && (!sharedLink.allowDownload || !sharedLink.showMetadata)) {
|
|
||||||
return getAssetThumbnailUrl({ id, size: AssetMediaSize.Preview, cacheKey });
|
|
||||||
}
|
|
||||||
|
|
||||||
return targetSize === 'original'
|
|
||||||
? getAssetOriginalUrl({ id, cacheKey })
|
|
||||||
: getAssetThumbnailUrl({ id, size: targetSize, cacheKey });
|
|
||||||
};
|
|
||||||
|
|
||||||
copyImage = async () => {
|
copyImage = async () => {
|
||||||
if (!canCopyImageToClipboard()) {
|
if (!canCopyImageToClipboard()) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await copyImageToClipboard($photoViewerImgElement ?? assetFileUrl);
|
await copyImageToClipboard($photoViewerImgElement ?? assetManager.url!);
|
||||||
notificationController.show({
|
notificationController.show({
|
||||||
type: NotificationType.Info,
|
type: NotificationType.Info,
|
||||||
message: $t('copied_image_to_clipboard'),
|
message: $t('copied_image_to_clipboard'),
|
||||||
@@ -108,17 +82,10 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
zoomToggle = () => {
|
|
||||||
photoZoomState.set({
|
|
||||||
...$photoZoomState,
|
|
||||||
currentZoom: $photoZoomState.currentZoom > 1 ? 1 : 2,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow);
|
const onPlaySlideshow = () => ($slideshowState = SlideshowState.PlaySlideshow);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (isFaceEditMode.value && $photoZoomState.currentZoom > 1) {
|
if (isFaceEditMode.value && zoomImageState && zoomImageState.currentZoom > 1) {
|
||||||
zoomToggle();
|
zoomToggle();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -132,7 +99,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const onSwipe = (event: SwipeCustomEvent) => {
|
const onSwipe = (event: SwipeCustomEvent) => {
|
||||||
if ($photoZoomState.currentZoom > 1) {
|
if (!zoomImageState || zoomImageState.currentZoom > 1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (onNextAsset && event.detail.direction === 'left') {
|
if (onNextAsset && event.detail.direction === 'left') {
|
||||||
@@ -143,21 +110,10 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// when true, will force loading of the original image
|
|
||||||
let forceUseOriginal: boolean = $derived(asset.originalMimeType === 'image/gif' || $photoZoomState.currentZoom > 1);
|
|
||||||
|
|
||||||
const targetImageSize = $derived.by(() => {
|
|
||||||
if ($alwaysLoadOriginalFile || forceUseOriginal || originalImageLoaded) {
|
|
||||||
return isWebCompatibleImage(asset) ? 'original' : AssetMediaSize.Fullsize;
|
|
||||||
}
|
|
||||||
|
|
||||||
return AssetMediaSize.Preview;
|
|
||||||
});
|
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (assetFileUrl) {
|
if (assetManager.url) {
|
||||||
// this can't be in an async context with $effect
|
// this can't be in an async context with $effect
|
||||||
void cast(assetFileUrl);
|
void cast(assetManager.url);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -175,35 +131,10 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onload = () => {
|
onDestroy(() => {
|
||||||
imageLoaded = true;
|
cancelImageLoad(assetManager);
|
||||||
assetFileUrl = imageLoaderUrl;
|
|
||||||
originalImageLoaded = targetImageSize === AssetMediaSize.Fullsize || targetImageSize === 'original';
|
|
||||||
};
|
|
||||||
|
|
||||||
const onerror = () => {
|
|
||||||
imageError = imageLoaded = true;
|
|
||||||
};
|
|
||||||
|
|
||||||
$effect(() => {
|
|
||||||
preload(targetImageSize, preloadAssets);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onMount(() => {
|
|
||||||
if (loader?.complete) {
|
|
||||||
onload();
|
|
||||||
}
|
|
||||||
loader?.addEventListener('load', onload, { passive: true });
|
|
||||||
loader?.addEventListener('error', onerror, { passive: true });
|
|
||||||
return () => {
|
|
||||||
loader?.removeEventListener('load', onload);
|
|
||||||
loader?.removeEventListener('error', onerror);
|
|
||||||
cancelImageUrl(imageLoaderUrl);
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
let imageLoaderUrl = $derived(getAssetUrl(asset.id, targetImageSize, asset.thumbhash));
|
|
||||||
|
|
||||||
let containerWidth = $state(0);
|
let containerWidth = $state(0);
|
||||||
let containerHeight = $state(0);
|
let containerHeight = $state(0);
|
||||||
</script>
|
</script>
|
||||||
@@ -217,27 +148,32 @@
|
|||||||
{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false },
|
{ shortcut: { key: 'z' }, onShortcut: zoomToggle, preventDefault: false },
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
{#if imageError}
|
{#if assetManager.loadError}
|
||||||
<div class="h-full w-full">
|
<div class="h-full w-full">
|
||||||
<BrokenAsset class="text-xl h-full w-full" />
|
<BrokenAsset class="text-xl h-full w-full" />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
<!-- svelte-ignore a11y_missing_attribute -->
|
|
||||||
<img bind:this={loader} style="display:none" src={imageLoaderUrl} aria-hidden="true" />
|
|
||||||
<div
|
<div
|
||||||
bind:this={element}
|
bind:this={element}
|
||||||
class="relative h-full select-none"
|
class="relative h-full select-none"
|
||||||
bind:clientWidth={containerWidth}
|
bind:clientWidth={containerWidth}
|
||||||
bind:clientHeight={containerHeight}
|
bind:clientHeight={containerHeight}
|
||||||
>
|
>
|
||||||
<img style="display:none" src={imageLoaderUrl} alt="" {onload} {onerror} />
|
<img
|
||||||
{#if !imageLoaded}
|
style="display:none"
|
||||||
|
src={assetManager.url}
|
||||||
|
alt=""
|
||||||
|
aria-hidden="true"
|
||||||
|
onload={() => mediaLoaded(assetManager)}
|
||||||
|
onerror={() => mediaLoadError(assetManager)}
|
||||||
|
/>
|
||||||
|
{#if !assetManager.isLoaded}
|
||||||
<div id="spinner" class="flex h-full items-center justify-center">
|
<div id="spinner" class="flex h-full items-center justify-center">
|
||||||
<LoadingSpinner />
|
<LoadingSpinner />
|
||||||
</div>
|
</div>
|
||||||
{:else if !imageError}
|
{:else if !assetManager.loadError}
|
||||||
<div
|
<div
|
||||||
use:zoomImageAction
|
{@attach zoomImageAttachment(assetManager)}
|
||||||
use:swipe={() => ({})}
|
use:swipe={() => ({})}
|
||||||
onswipe={onSwipe}
|
onswipe={onSwipe}
|
||||||
class="h-full w-full"
|
class="h-full w-full"
|
||||||
@@ -245,7 +181,7 @@
|
|||||||
>
|
>
|
||||||
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
{#if $slideshowState !== SlideshowState.None && $slideshowLook === SlideshowLook.BlurredBackground}
|
||||||
<img
|
<img
|
||||||
src={assetFileUrl}
|
src={assetManager.url}
|
||||||
alt=""
|
alt=""
|
||||||
class="-z-1 absolute top-0 start-0 object-cover h-full w-full blur-lg"
|
class="-z-1 absolute top-0 start-0 object-cover h-full w-full blur-lg"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
@@ -253,15 +189,15 @@
|
|||||||
{/if}
|
{/if}
|
||||||
<img
|
<img
|
||||||
bind:this={$photoViewerImgElement}
|
bind:this={$photoViewerImgElement}
|
||||||
src={assetFileUrl}
|
src={assetManager.url}
|
||||||
alt={$getAltText(toTimelineAsset(asset))}
|
alt={$getAltText(toTimelineAsset(assetManager.asset!))}
|
||||||
class="h-full w-full {$slideshowState === SlideshowState.None
|
class="h-full w-full {$slideshowState === SlideshowState.None
|
||||||
? 'object-contain'
|
? 'object-contain'
|
||||||
: slideshowLookCssMapping[$slideshowLook]}"
|
: slideshowLookCssMapping[$slideshowLook]}"
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
<!-- eslint-disable-next-line svelte/require-each-key -->
|
<!-- eslint-disable-next-line svelte/require-each-key -->
|
||||||
{#each getBoundingBox($boundingBoxesArray, $photoZoomState, $photoViewerImgElement) as boundingbox}
|
{#each getBoundingBox($boundingBoxesArray, zoomImageState!, $photoViewerImgElement) as boundingbox}
|
||||||
<div
|
<div
|
||||||
class="absolute border-solid border-white border-[3px] rounded-lg"
|
class="absolute border-solid border-white border-[3px] rounded-lg"
|
||||||
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
|
style="top: {boundingbox.top}px; left: {boundingbox.left}px; height: {boundingbox.height}px; width: {boundingbox.width}px;"
|
||||||
@@ -270,7 +206,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if isFaceEditMode.value}
|
{#if isFaceEditMode.value}
|
||||||
<FaceEditor htmlElement={$photoViewerImgElement} {containerWidth} {containerHeight} assetId={asset.id} />
|
<FaceEditor
|
||||||
|
htmlElement={$photoViewerImgElement}
|
||||||
|
{containerWidth}
|
||||||
|
{containerHeight}
|
||||||
|
assetId={assetManager.asset!.id}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte';
|
import VideoRemoteViewer from '$lib/components/asset-viewer/video-remote-viewer.svelte';
|
||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
import { assetViewerFadeDuration } from '$lib/constants';
|
import { assetViewerFadeDuration } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { castManager } from '$lib/managers/cast-manager.svelte';
|
import { castManager } from '$lib/managers/cast-manager.svelte';
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { loopVideo as loopVideoPreference, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
import { loopVideo as loopVideoPreference, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||||
@@ -16,9 +17,8 @@
|
|||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetId: string;
|
assetManager: AssetManager;
|
||||||
loopVideo: boolean;
|
loopVideo: boolean;
|
||||||
cacheKey: string | null;
|
|
||||||
onPreviousAsset?: () => void;
|
onPreviousAsset?: () => void;
|
||||||
onNextAsset?: () => void;
|
onNextAsset?: () => void;
|
||||||
onVideoEnded?: () => void;
|
onVideoEnded?: () => void;
|
||||||
@@ -27,9 +27,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let {
|
||||||
assetId,
|
assetManager = $bindable(),
|
||||||
loopVideo,
|
loopVideo,
|
||||||
cacheKey,
|
|
||||||
onPreviousAsset = () => {},
|
onPreviousAsset = () => {},
|
||||||
onNextAsset = () => {},
|
onNextAsset = () => {},
|
||||||
onVideoEnded = () => {},
|
onVideoEnded = () => {},
|
||||||
|
|||||||
@@ -3,12 +3,13 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { fade } from 'svelte/transition';
|
import { fade } from 'svelte/transition';
|
||||||
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetId: string;
|
assetManager: AssetManager;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { assetId }: Props = $props();
|
const { assetManager = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const modules = Promise.all([
|
const modules = Promise.all([
|
||||||
import('./photo-sphere-viewer-adapter.svelte').then((module) => module.default),
|
import('./photo-sphere-viewer-adapter.svelte').then((module) => module.default),
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { ProjectionType } from '$lib/constants';
|
|
||||||
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
|
import VideoNativeViewer from '$lib/components/asset-viewer/video-native-viewer.svelte';
|
||||||
import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte';
|
import VideoPanoramaViewer from '$lib/components/asset-viewer/video-panorama-viewer.svelte';
|
||||||
|
import { ProjectionType } from '$lib/constants';
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetId: string;
|
assetManager: AssetManager;
|
||||||
projectionType: string | null | undefined;
|
projectionType: string | null | undefined;
|
||||||
cacheKey: string | null;
|
|
||||||
loopVideo: boolean;
|
loopVideo: boolean;
|
||||||
onClose?: () => void;
|
onClose?: () => void;
|
||||||
onPreviousAsset?: () => void;
|
onPreviousAsset?: () => void;
|
||||||
@@ -15,10 +15,10 @@
|
|||||||
onVideoStarted?: () => void;
|
onVideoStarted?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO: do not preload assets.
|
||||||
let {
|
let {
|
||||||
assetId,
|
assetManager = $bindable(),
|
||||||
projectionType,
|
projectionType,
|
||||||
cacheKey,
|
|
||||||
loopVideo,
|
loopVideo,
|
||||||
onPreviousAsset,
|
onPreviousAsset,
|
||||||
onClose,
|
onClose,
|
||||||
@@ -29,12 +29,11 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
{#if projectionType === ProjectionType.EQUIRECTANGULAR}
|
||||||
<VideoPanoramaViewer {assetId} />
|
<VideoPanoramaViewer {assetManager} />
|
||||||
{:else}
|
{:else}
|
||||||
<VideoNativeViewer
|
<VideoNativeViewer
|
||||||
{loopVideo}
|
{loopVideo}
|
||||||
{cacheKey}
|
{assetManager}
|
||||||
{assetId}
|
|
||||||
{onPreviousAsset}
|
{onPreviousAsset}
|
||||||
{onNextAsset}
|
{onNextAsset}
|
||||||
{onVideoEnded}
|
{onVideoEnded}
|
||||||
|
|||||||
@@ -3,7 +3,6 @@
|
|||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
|
||||||
import { boundingBoxesArray } from '$lib/stores/people.store';
|
import { boundingBoxesArray } from '$lib/stores/people.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
@@ -20,6 +19,7 @@
|
|||||||
type AssetFaceResponseDto,
|
type AssetFaceResponseDto,
|
||||||
type PersonResponseDto,
|
type PersonResponseDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiAccountOff, mdiArrowLeftThin, mdiPencil, mdiRestart, mdiTrashCan } from '@mdi/js';
|
import { mdiAccountOff, mdiArrowLeftThin, mdiPencil, mdiRestart, mdiTrashCan } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@@ -28,7 +28,6 @@
|
|||||||
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
|
||||||
import { IconButton } from '@immich/ui';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assetId: string;
|
assetId: string;
|
||||||
@@ -184,7 +183,8 @@
|
|||||||
|
|
||||||
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
|
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
|
||||||
|
|
||||||
await assetViewingStore.setAssetId(assetId);
|
// TODO: manual tag face
|
||||||
|
// await assetViewingStore.setAssetId(assetId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('error_delete_face'));
|
handleError(error, $t('error_delete_face'));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,10 +26,10 @@
|
|||||||
NotificationType,
|
NotificationType,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
||||||
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
@@ -61,6 +61,12 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { Tween } from 'svelte/motion';
|
import { Tween } from 'svelte/motion';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
assetManager: AssetManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { assetManager = $bindable() }: Props = $props();
|
||||||
|
|
||||||
let memoryGallery: HTMLElement | undefined = $state();
|
let memoryGallery: HTMLElement | undefined = $state();
|
||||||
let memoryWrapper: HTMLElement | undefined = $state();
|
let memoryWrapper: HTMLElement | undefined = $state();
|
||||||
let galleryInView = $state(false);
|
let galleryInView = $state(false);
|
||||||
@@ -76,7 +82,6 @@
|
|||||||
let isSaved = $derived(current?.memory.isSaved);
|
let isSaved = $derived(current?.memory.isSaved);
|
||||||
let viewerHeight = $state(0);
|
let viewerHeight = $state(0);
|
||||||
|
|
||||||
const { isViewing } = assetViewingStore;
|
|
||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
// need to include padding in the viewport for gallery
|
// need to include padding in the viewport for gallery
|
||||||
const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
|
const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
|
||||||
@@ -85,7 +90,7 @@
|
|||||||
let videoPlayer: HTMLVideoElement | undefined = $state();
|
let videoPlayer: HTMLVideoElement | undefined = $state();
|
||||||
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
|
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
|
||||||
const handleNavigate = async (asset?: { id: string }) => {
|
const handleNavigate = async (asset?: { id: string }) => {
|
||||||
if ($isViewing) {
|
if (assetManager.showAssetViewer) {
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,7 +256,7 @@
|
|||||||
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
|
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($isViewing) {
|
if (assetManager.showAssetViewer) {
|
||||||
handlePromiseError(handleAction('initPlayer[AssetViewOpen]', 'pause'));
|
handlePromiseError(handleAction('initPlayer[AssetViewOpen]', 'pause'));
|
||||||
} else {
|
} else {
|
||||||
handlePromiseError(handleAction('initPlayer[AssetViewClosed]', 'reset'));
|
handlePromiseError(handleAction('initPlayer[AssetViewClosed]', 'reset'));
|
||||||
@@ -296,7 +301,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document
|
<svelte:document
|
||||||
use:shortcuts={$isViewing
|
use:shortcuts={assetManager.showAssetViewer
|
||||||
? []
|
? []
|
||||||
: [
|
: [
|
||||||
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => handleNextAsset() },
|
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => handleNextAsset() },
|
||||||
@@ -640,6 +645,7 @@
|
|||||||
bind:this={memoryGallery}
|
bind:this={memoryGallery}
|
||||||
>
|
>
|
||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
|
{assetManager}
|
||||||
onNext={handleNextAsset}
|
onNext={handleNextAsset}
|
||||||
onPrevious={handlePreviousAsset}
|
onPrevious={handlePreviousAsset}
|
||||||
assets={currentTimelineAssets}
|
assets={currentTimelineAssets}
|
||||||
|
|||||||
@@ -12,7 +12,7 @@
|
|||||||
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
|
||||||
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte';
|
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
@@ -20,7 +20,6 @@
|
|||||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
@@ -36,7 +35,7 @@
|
|||||||
type ScrubberListener,
|
type ScrubberListener,
|
||||||
type TimelinePlainYearMonth,
|
type TimelinePlainYearMonth,
|
||||||
} from '$lib/utils/timeline-util';
|
} from '$lib/utils/timeline-util';
|
||||||
import { AssetVisibility, getAssetInfo, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
import { AssetVisibility, type AlbumResponseDto, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
import { onMount, type Snippet } from 'svelte';
|
import { onMount, type Snippet } from 'svelte';
|
||||||
import type { UpdatePayload } from 'vite';
|
import type { UpdatePayload } from 'vite';
|
||||||
@@ -53,6 +52,7 @@
|
|||||||
enableRouting: boolean;
|
enableRouting: boolean;
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
|
assetManager: AssetManager;
|
||||||
removeAction?:
|
removeAction?:
|
||||||
| AssetAction.UNARCHIVE
|
| AssetAction.UNARCHIVE
|
||||||
| AssetAction.ARCHIVE
|
| AssetAction.ARCHIVE
|
||||||
@@ -78,6 +78,7 @@
|
|||||||
enableRouting,
|
enableRouting,
|
||||||
timelineManager = $bindable(),
|
timelineManager = $bindable(),
|
||||||
assetInteraction,
|
assetInteraction,
|
||||||
|
assetManager = $bindable(),
|
||||||
removeAction = null,
|
removeAction = null,
|
||||||
withStacked = false,
|
withStacked = false,
|
||||||
showArchiveIcon = false,
|
showArchiveIcon = false,
|
||||||
@@ -91,8 +92,6 @@
|
|||||||
empty,
|
empty,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
|
|
||||||
|
|
||||||
let element: HTMLElement | undefined = $state();
|
let element: HTMLElement | undefined = $state();
|
||||||
|
|
||||||
let timelineElement: HTMLElement | undefined = $state();
|
let timelineElement: HTMLElement | undefined = $state();
|
||||||
@@ -103,6 +102,8 @@
|
|||||||
let scrubOverallPercent: number = $state(0);
|
let scrubOverallPercent: number = $state(0);
|
||||||
let scrubberWidth = $state(0);
|
let scrubberWidth = $state(0);
|
||||||
|
|
||||||
|
let asset = $derived(assetManager.asset);
|
||||||
|
|
||||||
// 60 is the bottom spacer element at 60px
|
// 60 is the bottom spacer element at 60px
|
||||||
let bottomSectionHeight = 60;
|
let bottomSectionHeight = 60;
|
||||||
let leadout = $state(false);
|
let leadout = $state(false);
|
||||||
@@ -177,7 +178,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const completeNav = async () => {
|
const completeNav = async () => {
|
||||||
const scrollTarget = $gridScrollTarget?.at;
|
const scrollTarget = assetManager.gridScrollTarget?.at;
|
||||||
let scrolled = false;
|
let scrolled = false;
|
||||||
if (scrollTarget) {
|
if (scrollTarget) {
|
||||||
scrolled = await scrollToAssetId(scrollTarget);
|
scrolled = await scrollToAssetId(scrollTarget);
|
||||||
@@ -212,9 +213,9 @@
|
|||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
const asset = $page.url.searchParams.get('at');
|
const asset = $page.url.searchParams.get('at');
|
||||||
if (asset) {
|
if (asset) {
|
||||||
$gridScrollTarget = { at: asset };
|
assetManager.gridScrollTarget = { at: asset };
|
||||||
void navigate(
|
void navigate(
|
||||||
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget },
|
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: assetManager.gridScrollTarget },
|
||||||
{ replaceState: true, forceNavigate: true },
|
{ replaceState: true, forceNavigate: true },
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -438,46 +439,50 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const handlePrevious = async () => {
|
const handlePrevious = async () => {
|
||||||
const laterAsset = await timelineManager.getLaterAsset($viewingAsset);
|
let laterAsset = undefined;
|
||||||
|
if (asset) {
|
||||||
if (laterAsset) {
|
laterAsset = await timelineManager.getLaterAsset(asset);
|
||||||
const preloadAsset = await timelineManager.getLaterAsset(laterAsset);
|
if (laterAsset) {
|
||||||
const asset = await getAssetInfo({ id: laterAsset.id, key: authManager.key });
|
// TODO: If preloadAsset is undefined, throw an exception.
|
||||||
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
// assetManager.preloadAssets = [await timelineManager.getLaterAsset(laterAsset)];
|
||||||
await navigate({ targetRoute: 'current', assetId: laterAsset.id });
|
await navigate({ targetRoute: 'current', assetId: laterAsset.id });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!laterAsset;
|
return !!laterAsset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleNext = async () => {
|
const handleNext = async () => {
|
||||||
const earlierAsset = await timelineManager.getEarlierAsset($viewingAsset);
|
let earlierAsset = undefined;
|
||||||
if (earlierAsset) {
|
if (asset) {
|
||||||
const preloadAsset = await timelineManager.getEarlierAsset(earlierAsset);
|
earlierAsset = await timelineManager.getEarlierAsset(asset);
|
||||||
const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key });
|
if (earlierAsset) {
|
||||||
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
|
// assetManager.preloadAssets = [await timelineManager.getEarlierAsset(earlierAsset)];
|
||||||
await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
|
await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!earlierAsset;
|
return !!earlierAsset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRandom = async () => {
|
const handleRandom = async () => {
|
||||||
const randomAsset = await timelineManager.getRandomAsset();
|
let randomAsset = undefined;
|
||||||
|
if (asset) {
|
||||||
if (randomAsset) {
|
randomAsset = await timelineManager.getRandomAsset();
|
||||||
const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key });
|
if (randomAsset) {
|
||||||
assetViewingStore.setAsset(asset);
|
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
|
||||||
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
|
}
|
||||||
return asset;
|
|
||||||
}
|
}
|
||||||
|
return !!randomAsset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleClose = async (asset: { id: string }) => {
|
const handleClose = async (asset: { id: string }) => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetManager.showAssetViewer = false;
|
||||||
showSkeleton = true;
|
showSkeleton = true;
|
||||||
$gridScrollTarget = { at: asset.id };
|
assetManager.gridScrollTarget = { at: asset.id };
|
||||||
await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget });
|
await navigate({
|
||||||
|
targetRoute: 'current',
|
||||||
|
assetId: null,
|
||||||
|
assetGridRouteSearchParams: assetManager.gridScrollTarget,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const handlePreAction = async (action: Action) => {
|
const handlePreAction = async (action: Action) => {
|
||||||
@@ -722,7 +727,7 @@
|
|||||||
|
|
||||||
let shortcutList = $derived(
|
let shortcutList = $derived(
|
||||||
(() => {
|
(() => {
|
||||||
if (searchStore.isSearchEnabled || $showAssetViewer) {
|
if (searchStore.isSearchEnabled || assetManager.showAssetViewer) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -775,8 +780,8 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($showAssetViewer) {
|
if (assetManager.showAssetViewer && asset) {
|
||||||
const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60);
|
const { localDateTime } = getTimes(asset.fileCreatedAt, DateTime.local().offset / 60);
|
||||||
void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month });
|
void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -923,12 +928,11 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
{#if $showAssetViewer}
|
{#if assetManager.showAssetViewer}
|
||||||
{#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
{#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
|
{assetManager}
|
||||||
{withStacked}
|
{withStacked}
|
||||||
asset={$viewingAsset}
|
|
||||||
preloadAssets={$preloadAssets}
|
|
||||||
{isShared}
|
{isShared}
|
||||||
{album}
|
{album}
|
||||||
{person}
|
{person}
|
||||||
|
|||||||
@@ -3,16 +3,18 @@
|
|||||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
|
||||||
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { addSharedLinkAssets, getAssetInfo, type SharedLinkResponseDto } from '@immich/sdk';
|
import { addSharedLinkAssets, type SharedLinkResponseDto } from '@immich/sdk';
|
||||||
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
|
||||||
@@ -22,14 +24,14 @@
|
|||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
import { NotificationType, notificationController } from '../shared-components/notification/notification';
|
||||||
import { IconButton } from '@immich/ui';
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
assetManager: AssetManager;
|
||||||
sharedLink: SharedLinkResponseDto;
|
sharedLink: SharedLinkResponseDto;
|
||||||
isOwned: boolean;
|
isOwned: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { sharedLink = $bindable(), isOwned }: Props = $props();
|
let { assetManager = $bindable(), sharedLink = $bindable(), isOwned }: Props = $props();
|
||||||
|
|
||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
@@ -86,6 +88,13 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
// TODO: defer init until trigger updateOptions
|
||||||
|
if (assets.length === 1) {
|
||||||
|
void assetManager.updateOptions({ assetId: assets[0].id });
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<section>
|
<section>
|
||||||
@@ -142,19 +151,17 @@
|
|||||||
</ControlAppBar>
|
</ControlAppBar>
|
||||||
{/if}
|
{/if}
|
||||||
<section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
|
<section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
|
||||||
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
<GalleryViewer {assets} {assetInteraction} {assetManager} {viewport} />
|
||||||
</section>
|
</section>
|
||||||
{:else if assets.length === 1}
|
{:else if assets.length === 1}
|
||||||
{#await getAssetInfo({ id: assets[0].id, key: authManager.key }) then asset}
|
<AssetViewer
|
||||||
<AssetViewer
|
{assetManager}
|
||||||
{asset}
|
showCloseButton={false}
|
||||||
showCloseButton={false}
|
onAction={handleAction}
|
||||||
onAction={handleAction}
|
onPrevious={() => Promise.resolve(false)}
|
||||||
onPrevious={() => Promise.resolve(false)}
|
onNext={() => Promise.resolve(false)}
|
||||||
onNext={() => Promise.resolve(false)}
|
onRandom={() => Promise.resolve(false)}
|
||||||
onRandom={() => Promise.resolve(undefined)}
|
onClose={() => {}}
|
||||||
onClose={() => {}}
|
/>
|
||||||
/>
|
|
||||||
{/await}
|
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -4,11 +4,11 @@
|
|||||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
import ShortcutsModal from '$lib/modals/ShortcutsModal.svelte';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
@@ -29,6 +29,7 @@
|
|||||||
interface Props {
|
interface Props {
|
||||||
assets: (TimelineAsset | AssetResponseDto)[];
|
assets: (TimelineAsset | AssetResponseDto)[];
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
|
assetManager: AssetManager;
|
||||||
disableAssetSelect?: boolean;
|
disableAssetSelect?: boolean;
|
||||||
showArchiveIcon?: boolean;
|
showArchiveIcon?: boolean;
|
||||||
viewport: Viewport;
|
viewport: Viewport;
|
||||||
@@ -46,6 +47,7 @@
|
|||||||
let {
|
let {
|
||||||
assets = $bindable(),
|
assets = $bindable(),
|
||||||
assetInteraction,
|
assetInteraction,
|
||||||
|
assetManager = $bindable(),
|
||||||
disableAssetSelect = false,
|
disableAssetSelect = false,
|
||||||
showArchiveIcon = false,
|
showArchiveIcon = false,
|
||||||
viewport,
|
viewport,
|
||||||
@@ -60,8 +62,6 @@
|
|||||||
pageHeaderOffset = 0,
|
pageHeaderOffset = 0,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
|
|
||||||
|
|
||||||
let geometry: CommonJustifiedLayout | undefined = $state();
|
let geometry: CommonJustifiedLayout | undefined = $state();
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
@@ -151,8 +151,7 @@
|
|||||||
});
|
});
|
||||||
const viewAssetHandler = async (asset: TimelineAsset) => {
|
const viewAssetHandler = async (asset: TimelineAsset) => {
|
||||||
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
||||||
await setAssetId(assets[currentViewAssetIndex].id);
|
await navigate({ targetRoute: 'current', assetId: assets[currentViewAssetIndex].id });
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAllAssets = () => {
|
const selectAllAssets = () => {
|
||||||
@@ -292,7 +291,7 @@
|
|||||||
|
|
||||||
const shortcutList = $derived(
|
const shortcutList = $derived(
|
||||||
(() => {
|
(() => {
|
||||||
if ($isViewerOpen) {
|
if (assetManager.showAssetViewer) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -344,7 +343,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRandom = async (): Promise<{ id: string } | undefined> => {
|
const handleRandom = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
let asset: { id: string } | undefined;
|
let asset: { id: string } | undefined;
|
||||||
if (onRandom) {
|
if (onRandom) {
|
||||||
@@ -357,14 +356,14 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
await navigateToAsset(asset);
|
await navigateToAsset(asset);
|
||||||
return asset;
|
return true;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
handleError(error, $t('errors.cannot_navigate_next_asset'));
|
handleError(error, $t('errors.cannot_navigate_next_asset'));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -395,9 +394,8 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const navigateToAsset = async (asset?: { id: string }) => {
|
const navigateToAsset = async (asset?: { id: string }) => {
|
||||||
if (asset && asset.id !== $viewingAsset.id) {
|
if (asset && asset.id !== assetManager.asset.id) {
|
||||||
await setAssetId(asset.id);
|
await navigate({ targetRoute: 'current', assetId: asset.id });
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -415,7 +413,7 @@
|
|||||||
} else if (currentViewAssetIndex === assets.length) {
|
} else if (currentViewAssetIndex === assets.length) {
|
||||||
await handlePrevious();
|
await handlePrevious();
|
||||||
} else {
|
} else {
|
||||||
await setAssetId(assets[currentViewAssetIndex].id);
|
await navigate({ targetRoute: 'current', assetId: assets[currentViewAssetIndex].id });
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -512,16 +510,16 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<!-- Overlay Asset Viewer -->
|
<!-- Overlay Asset Viewer -->
|
||||||
{#if $isViewerOpen}
|
{#if assetManager.showAssetViewer}
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
asset={$viewingAsset}
|
{assetManager}
|
||||||
onAction={handleAction}
|
onAction={handleAction}
|
||||||
onPrevious={handlePrevious}
|
onPrevious={handlePrevious}
|
||||||
onNext={handleNext}
|
onNext={handleNext}
|
||||||
onRandom={handleRandom}
|
onRandom={handleRandom}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetManager.showAssetViewer = false;
|
||||||
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
import { shortcuts } from '$lib/actions/shortcut';
|
import { shortcuts } from '$lib/actions/shortcut';
|
||||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
|
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import type { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { suggestDuplicate } from '$lib/utils/duplicate-utils';
|
import { suggestDuplicate } from '$lib/utils/duplicate-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
@@ -15,12 +15,12 @@
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assets: AssetResponseDto[];
|
assets: AssetResponseDto[];
|
||||||
|
assetManager: AssetManager;
|
||||||
onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
|
onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
|
||||||
onStack: (assets: AssetResponseDto[]) => void;
|
onStack: (assets: AssetResponseDto[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { assets, onResolve, onStack }: Props = $props();
|
let { assets, assetManager, onResolve, onStack }: Props = $props();
|
||||||
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
|
|
||||||
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
|
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
|
||||||
|
|
||||||
// eslint-disable-next-line svelte/no-unnecessary-state-wrap
|
// eslint-disable-next-line svelte/no-unnecessary-state-wrap
|
||||||
@@ -39,35 +39,34 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetManager.showAssetViewer = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
const onNext = () => {
|
const onNext = async () => {
|
||||||
const index = getAssetIndex($viewingAsset.id) + 1;
|
const index = getAssetIndex(assetManager.asset.id) + 1;
|
||||||
if (index >= assets.length) {
|
if (index >= assets.length) {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
setAsset(assets[index]);
|
await navigate({ targetRoute: 'current', assetId: assets[index].id });
|
||||||
return Promise.resolve(true);
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onPrevious = () => {
|
const onPrevious = async () => {
|
||||||
const index = getAssetIndex($viewingAsset.id) - 1;
|
const index = getAssetIndex(assetManager.asset.id) - 1;
|
||||||
if (index < 0) {
|
if (index < 0) {
|
||||||
return Promise.resolve(false);
|
return false;
|
||||||
}
|
}
|
||||||
setAsset(assets[index]);
|
await navigate({ targetRoute: 'current', assetId: assets[index].id });
|
||||||
return Promise.resolve(true);
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const onRandom = () => {
|
const onRandom = async () => {
|
||||||
if (assets.length <= 0) {
|
if (assets.length <= 0) {
|
||||||
return Promise.resolve(undefined);
|
return false;
|
||||||
}
|
}
|
||||||
const index = Math.floor(Math.random() * assets.length);
|
const index = Math.floor(Math.random() * assets.length);
|
||||||
const asset = assets[index];
|
await navigate({ targetRoute: 'current', assetId: assets[index].id });
|
||||||
setAsset(asset);
|
return true;
|
||||||
return Promise.resolve(asset);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const onSelectAsset = (asset: AssetResponseDto) => {
|
const onSelectAsset = (asset: AssetResponseDto) => {
|
||||||
@@ -102,9 +101,7 @@
|
|||||||
{ shortcut: { key: 'a' }, onShortcut: onSelectAll },
|
{ shortcut: { key: 'a' }, onShortcut: onSelectAll },
|
||||||
{
|
{
|
||||||
shortcut: { key: 's' },
|
shortcut: { key: 's' },
|
||||||
onShortcut: () => {
|
onShortcut: () => navigate({ targetRoute: 'current', assetId: assets[0].id }),
|
||||||
setAsset(assets[0]);
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
|
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
|
||||||
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
|
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
|
||||||
@@ -170,23 +167,23 @@
|
|||||||
{asset}
|
{asset}
|
||||||
{onSelectAsset}
|
{onSelectAsset}
|
||||||
isSelected={selectedAssetIds.has(asset.id)}
|
isSelected={selectedAssetIds.has(asset.id)}
|
||||||
onViewAsset={(asset) => setAsset(asset)}
|
onViewAsset={(asset) => navigate({ targetRoute: 'current', assetId: asset.id })}
|
||||||
/>
|
/>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if $showAssetViewer}
|
{#if assetManager.showAssetViewer}
|
||||||
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
asset={$viewingAsset}
|
{assetManager}
|
||||||
showNavigation={assets.length > 1}
|
showNavigation={assets.length > 1}
|
||||||
{onNext}
|
{onNext}
|
||||||
{onPrevious}
|
{onPrevious}
|
||||||
{onRandom}
|
{onRandom}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetManager.showAssetViewer = false;
|
||||||
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
202
web/src/lib/managers/asset-manager/asset-manager.svelte.ts
Normal file
202
web/src/lib/managers/asset-manager/asset-manager.svelte.ts
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
import type { AssetPackage } from '$lib/managers/asset-manager/asset-package.svelte';
|
||||||
|
import { loadFromAssetPackage } from '$lib/managers/asset-manager/internal/load-support.svelte';
|
||||||
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
|
import type { AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
||||||
|
import { type ZoomImageWheelState } from '@zoom-image/core';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import { LRUCache } from 'lru-cache';
|
||||||
|
|
||||||
|
export enum AssetMediaSize {
|
||||||
|
Original = 'original',
|
||||||
|
Fullsize = 'fullsize',
|
||||||
|
Preview = 'preview',
|
||||||
|
Thumbnail = 'thumbnail',
|
||||||
|
Playback = 'playback',
|
||||||
|
}
|
||||||
|
|
||||||
|
export type LoadAssetOptions = {
|
||||||
|
loadAlbums?: boolean;
|
||||||
|
loadStack?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type AssetManagerOptions = {};
|
||||||
|
|
||||||
|
export class AssetManager {
|
||||||
|
isInitialized = $state(false);
|
||||||
|
isLoaded = $state(false);
|
||||||
|
loadError = $state(false);
|
||||||
|
// The queue waited for load. The first is the currect and the next is preload.
|
||||||
|
// The preload asset is not need to loading immediately.
|
||||||
|
assetLoadingQueue: AssetPackage[] = $state([]);
|
||||||
|
|
||||||
|
// url: string | undefined = $derived.by(() => {
|
||||||
|
// if (this.asset) {
|
||||||
|
// return this.#getAssetUrl(toTimelineAsset(this.asset!));
|
||||||
|
// }
|
||||||
|
// });
|
||||||
|
|
||||||
|
#maximumLRUCache: number = $state(10);
|
||||||
|
|
||||||
|
// TODO: This function is used to test.
|
||||||
|
dispose(value: AssetPackage, key: string) {
|
||||||
|
console.log(key);
|
||||||
|
console.log(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
assetCache: LRUCache<string, AssetPackage> = $state(
|
||||||
|
new LRUCache({ max: this.#maximumLRUCache, dispose: this.dispose }),
|
||||||
|
);
|
||||||
|
|
||||||
|
showAssetViewer: boolean = $state(false);
|
||||||
|
gridScrollTarget: AssetGridRouteSearchParams | undefined = $state();
|
||||||
|
zoomImageState: ZoomImageWheelState | undefined = $state();
|
||||||
|
|
||||||
|
initTask = new CancellableTask(
|
||||||
|
() => (this.isInitialized = true),
|
||||||
|
() => {
|
||||||
|
this.assetLoadingQueue = [];
|
||||||
|
this.assetCache.clear();
|
||||||
|
this.isInitialized = false;
|
||||||
|
},
|
||||||
|
() => void 0,
|
||||||
|
);
|
||||||
|
|
||||||
|
static #INIT_OPTIONS = {};
|
||||||
|
#options: AssetManagerOptions = AssetManager.#INIT_OPTIONS;
|
||||||
|
|
||||||
|
static #DEFAULT_LOAD_ASSET_OPTIONS: LoadAssetOptions = {
|
||||||
|
loadAlbums: false,
|
||||||
|
loadStack: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
constructor() {}
|
||||||
|
|
||||||
|
async loadAssetPackage(options?: LoadAssetOptions, cancelable?: boolean): Promise<void> {
|
||||||
|
cancelable = cancelable ?? true;
|
||||||
|
options = options ?? AssetManager.#DEFAULT_LOAD_ASSET_OPTIONS;
|
||||||
|
|
||||||
|
const assetPackage = this.assetLoadingQueue[0];
|
||||||
|
if (!assetPackage) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (assetPackage.loader?.executed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await assetPackage.loader?.execute(async (signal: AbortSignal) => {
|
||||||
|
await loadFromAssetPackage(this, assetPackage, options, signal);
|
||||||
|
}, cancelable);
|
||||||
|
}
|
||||||
|
|
||||||
|
async #initializeAsset() {
|
||||||
|
// TODO: Preload assets.
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
this.assetLoadingQueue = [];
|
||||||
|
this.assetCache.clear();
|
||||||
|
await this.initTask.execute(async () => {
|
||||||
|
this.#options = options;
|
||||||
|
await this.#initializeAsset();
|
||||||
|
}, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
public destroy() {
|
||||||
|
this.isInitialized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// #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.
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// #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 });
|
||||||
|
// });
|
||||||
48
web/src/lib/managers/asset-manager/asset-package.svelte.ts
Normal file
48
web/src/lib/managers/asset-manager/asset-package.svelte.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
|
import type { AlbumResponseDto, AssetResponseDto, StackResponseDto } from '@immich/sdk';
|
||||||
|
import { t } from 'svelte-i18n';
|
||||||
|
import { get } from 'svelte/store';
|
||||||
|
import type { AssetManager, AssetManagerOptions } from './asset-manager.svelte';
|
||||||
|
|
||||||
|
export class AssetPackage {
|
||||||
|
isLoaded: boolean = $state(false);
|
||||||
|
asset: AssetResponseDto | undefined = $state();
|
||||||
|
albums: AlbumResponseDto[] = $state([]);
|
||||||
|
stack: StackResponseDto | undefined = $state();
|
||||||
|
readonly assetId: string;
|
||||||
|
readonly assetManager: AssetManager;
|
||||||
|
|
||||||
|
// To ensure albums and stack is need to reloading.
|
||||||
|
options: AssetManagerOptions | undefined = $state();
|
||||||
|
|
||||||
|
loader: CancellableTask | undefined;
|
||||||
|
|
||||||
|
constructor(store: AssetManager, assetId: string) {
|
||||||
|
this.assetManager = store;
|
||||||
|
this.assetId = assetId;
|
||||||
|
|
||||||
|
this.loader = new CancellableTask(
|
||||||
|
() => {
|
||||||
|
this.isLoaded = true;
|
||||||
|
},
|
||||||
|
() => {
|
||||||
|
this.asset = undefined;
|
||||||
|
this.albums = [];
|
||||||
|
this.stack = undefined;
|
||||||
|
this.isLoaded = false;
|
||||||
|
},
|
||||||
|
this.#handleLoadError,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Add error message to translation.
|
||||||
|
#handleLoadError(error: unknown) {
|
||||||
|
const _$t = get(t);
|
||||||
|
handleError(error, _$t('errors.failed_to_load_asset'));
|
||||||
|
}
|
||||||
|
|
||||||
|
cancel() {
|
||||||
|
this.loader?.cancel();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
import type { AssetManager, LoadAssetOptions } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
import { AssetPackage } from '$lib/managers/asset-manager/asset-package.svelte';
|
||||||
|
import { authManager } from '$lib/managers/auth-manager.svelte';
|
||||||
|
// import { cancelImageUrl } from '$lib/utils/sw-messaging';
|
||||||
|
import { getAllAlbums, getAssetInfo, getStack } from '@immich/sdk';
|
||||||
|
|
||||||
|
export async function loadFromAssetPackage(
|
||||||
|
assetManager: AssetManager,
|
||||||
|
assetPackage: AssetPackage,
|
||||||
|
options: LoadAssetOptions,
|
||||||
|
signal: AbortSignal,
|
||||||
|
): Promise<void> {
|
||||||
|
const assetId = assetPackage.assetId;
|
||||||
|
const assetCache = assetManager.assetCache.get(assetId);
|
||||||
|
if (assetCache && assetCache.options === options) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Compare between assetCache and assetCache.options to ensure whether we need update or not.
|
||||||
|
|
||||||
|
// If there is assetCache, then asset info is not need to update.
|
||||||
|
if (!assetCache) {
|
||||||
|
const key = authManager.key;
|
||||||
|
const assetResponse = await getAssetInfo(
|
||||||
|
{
|
||||||
|
id: assetId,
|
||||||
|
key,
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!assetResponse) {
|
||||||
|
throw new Error('get AssetInfo error');
|
||||||
|
}
|
||||||
|
assetPackage.asset = assetResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: need to update albums
|
||||||
|
if (options.loadAlbums) {
|
||||||
|
const albumsResponse = await getAllAlbums(
|
||||||
|
{
|
||||||
|
assetId,
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!albumsResponse) {
|
||||||
|
throw new Error('get AllAlbums error');
|
||||||
|
}
|
||||||
|
assetPackage.albums = albumsResponse;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (options.loadStack) {
|
||||||
|
const stackResponse = await getStack(
|
||||||
|
{
|
||||||
|
id: assetId,
|
||||||
|
},
|
||||||
|
{ signal },
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!stackResponse) {
|
||||||
|
throw new Error('get Stack error');
|
||||||
|
}
|
||||||
|
assetPackage.stack = stackResponse;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
3
web/src/lib/managers/asset-manager/utils.svelte.ts
Normal file
3
web/src/lib/managers/asset-manager/utils.svelte.ts
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
import type { AssetPackage } from '$lib/managers/asset-manager/asset-package.svelte';
|
||||||
|
|
||||||
|
export const assetPackage = (assetPackage: AssetPackage): AssetPackage => $state.snapshot(assetPackage);
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
import { authManager } from '$lib/managers/auth-manager.svelte';
|
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
|
||||||
import { type AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
|
||||||
import { getAssetInfo, type AssetResponseDto } from '@immich/sdk';
|
|
||||||
import { readonly, writable } from 'svelte/store';
|
|
||||||
|
|
||||||
function createAssetViewingStore() {
|
|
||||||
const viewingAssetStoreState = writable<AssetResponseDto>();
|
|
||||||
const preloadAssets = writable<TimelineAsset[]>([]);
|
|
||||||
const viewState = writable<boolean>(false);
|
|
||||||
const gridScrollTarget = writable<AssetGridRouteSearchParams | null | undefined>();
|
|
||||||
|
|
||||||
const setAsset = (asset: AssetResponseDto, assetsToPreload: TimelineAsset[] = []) => {
|
|
||||||
preloadAssets.set(assetsToPreload);
|
|
||||||
viewingAssetStoreState.set(asset);
|
|
||||||
viewState.set(true);
|
|
||||||
};
|
|
||||||
|
|
||||||
const setAssetId = async (id: string): Promise<AssetResponseDto> => {
|
|
||||||
const asset = await getAssetInfo({ id, key: authManager.key });
|
|
||||||
setAsset(asset);
|
|
||||||
return asset;
|
|
||||||
};
|
|
||||||
|
|
||||||
const showAssetViewer = (show: boolean) => {
|
|
||||||
viewState.set(show);
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
asset: readonly(viewingAssetStoreState),
|
|
||||||
preloadAssets: readonly(preloadAssets),
|
|
||||||
isViewing: viewState,
|
|
||||||
gridScrollTarget,
|
|
||||||
setAsset,
|
|
||||||
setAssetId,
|
|
||||||
showAssetViewer,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export const assetViewingStore = createAssetViewingStore();
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
import type { ZoomImageWheelState } from '@zoom-image/core';
|
|
||||||
import { writable } from 'svelte/store';
|
|
||||||
|
|
||||||
export const photoZoomState = writable<ZoomImageWheelState>();
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { getAssetInfo } from '@immich/sdk';
|
|
||||||
import type { NavigationTarget } from '@sveltejs/kit';
|
import type { NavigationTarget } from '@sveltejs/kit';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
@@ -22,10 +21,6 @@ export const isLockedFolderRoute = (route?: string | null) => !!route?.startsWit
|
|||||||
export const isAssetViewerRoute = (target?: NavigationTarget | null) =>
|
export const isAssetViewerRoute = (target?: NavigationTarget | null) =>
|
||||||
!!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {}));
|
!!(target?.route.id?.endsWith('/[[assetId=id]]') && 'assetId' in (target?.params || {}));
|
||||||
|
|
||||||
export function getAssetInfoFromParam({ assetId, key }: { assetId?: string; key?: string }) {
|
|
||||||
return assetId ? getAssetInfo({ id: assetId, key }) : undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
function currentUrlWithoutAsset() {
|
function currentUrlWithoutAsset() {
|
||||||
const $page = get(page);
|
const $page = get(page);
|
||||||
// This contains special casing for the /photos/:assetId route, which hangs directly
|
// This contains special casing for the /photos/:assetId route, which hangs directly
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ export class SlideshowHistory {
|
|||||||
private history: { id: string }[] = [];
|
private history: { id: string }[] = [];
|
||||||
private index = 0;
|
private index = 0;
|
||||||
|
|
||||||
constructor(private onChange: (asset: { id: string }) => void) {}
|
constructor(private onChange: (asset: { id: string }) => Promise<void>) {}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.history = [];
|
this.history = [];
|
||||||
@@ -18,23 +18,23 @@ export class SlideshowHistory {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
next(): boolean {
|
async next(): Promise<boolean> {
|
||||||
if (this.index === this.history.length - 1) {
|
if (this.index === this.history.length - 1) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.index++;
|
this.index++;
|
||||||
this.onChange(this.history[this.index]);
|
await this.onChange(this.history[this.index]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
previous(): boolean {
|
async previous(): Promise<boolean> {
|
||||||
if (this.index === 0) {
|
if (this.index === 0) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
this.index--;
|
this.index--;
|
||||||
this.onChange(this.history[this.index]);
|
await this.onChange(this.history[this.index]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +1,23 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { run } from 'svelte/legacy';
|
|
||||||
|
|
||||||
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
import UploadCover from '$lib/components/shared-components/drag-and-drop-upload-overlay.svelte';
|
||||||
import { page } from '$app/stores';
|
|
||||||
|
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import type { Snippet } from 'svelte';
|
import type { Snippet } from 'svelte';
|
||||||
interface Props {
|
interface Props {
|
||||||
children?: Snippet;
|
children?: Snippet;
|
||||||
}
|
}
|
||||||
|
|
||||||
let { children }: Props = $props();
|
let { children }: Props = $props();
|
||||||
let { isViewing: showAssetViewer, setAsset, gridScrollTarget } = assetViewingStore;
|
|
||||||
|
|
||||||
// $page.data.asset is loaded by route specific +page.ts loaders if that
|
// page.data.asset is loaded by route specific +page.ts loaders if that
|
||||||
// route contains the assetId path.
|
// route contains the assetId path.
|
||||||
run(() => {
|
// $effect(() => {
|
||||||
if ($page.data.asset) {
|
// TODO: navigation to the asset grid.
|
||||||
setAsset($page.data.asset);
|
// const asset = page.url.searchParams.get('at');
|
||||||
} else {
|
// gridScrollTarget = { at: asset };
|
||||||
$showAssetViewer = false;
|
// });
|
||||||
}
|
|
||||||
const asset = $page.url.searchParams.get('at');
|
|
||||||
$gridScrollTarget = { at: asset };
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class:display-none={$showAssetViewer}>
|
<!-- display-none is based on assetManager.showAssetViewer -->
|
||||||
|
<div class:display-none={false}>
|
||||||
{@render children?.()}
|
{@render children?.()}
|
||||||
</div>
|
</div>
|
||||||
<UploadCover />
|
<UploadCover />
|
||||||
|
|||||||
@@ -34,6 +34,7 @@
|
|||||||
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
|
||||||
import { AlbumPageViewMode, AppRoute } from '$lib/constants';
|
import { AlbumPageViewMode, AppRoute } from '$lib/constants';
|
||||||
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
import { activityManager } from '$lib/managers/activity-manager.svelte';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
@@ -43,7 +44,6 @@
|
|||||||
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
|
||||||
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
@@ -95,7 +95,6 @@
|
|||||||
|
|
||||||
let { data = $bindable() }: Props = $props();
|
let { data = $bindable() }: Props = $props();
|
||||||
|
|
||||||
let { isViewing: showAssetViewer, setAssetId, gridScrollTarget } = assetViewingStore;
|
|
||||||
let { slideshowState, slideshowNavigation } = slideshowStore;
|
let { slideshowState, slideshowNavigation } = slideshowStore;
|
||||||
|
|
||||||
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
|
||||||
@@ -109,6 +108,15 @@
|
|||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
const timelineInteraction = new AssetInteraction();
|
const timelineInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
afterNavigate(({ from }) => {
|
afterNavigate(({ from }) => {
|
||||||
let url: string | undefined = from?.url?.pathname;
|
let url: string | undefined = from?.url?.pathname;
|
||||||
|
|
||||||
@@ -148,7 +156,8 @@
|
|||||||
? await timelineManager.getRandomAsset()
|
? await timelineManager.getRandomAsset()
|
||||||
: timelineManager.months[0]?.dayGroups[0]?.viewerAssets[0]?.asset;
|
: timelineManager.months[0]?.dayGroups[0]?.viewerAssets[0]?.asset;
|
||||||
if (asset) {
|
if (asset) {
|
||||||
handlePromiseError(setAssetId(asset.id).then(() => ($slideshowState = SlideshowState.PlaySlideshow)));
|
await navigate({ targetRoute: 'current', assetId: asset.id });
|
||||||
|
$slideshowState = SlideshowState.PlaySlideshow;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -166,7 +175,7 @@
|
|||||||
viewMode = AlbumPageViewMode.VIEW;
|
viewMode = AlbumPageViewMode.VIEW;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if ($showAssetViewer) {
|
if (assetManager.showAssetViewer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
@@ -346,7 +355,7 @@
|
|||||||
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
|
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if ($showAssetViewer || !isShared) {
|
if (assetManager.showAssetViewer || !isShared) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +370,9 @@
|
|||||||
let isOwned = $derived($user.id == album.ownerId);
|
let isOwned = $derived($user.id == album.ownerId);
|
||||||
|
|
||||||
let showActivityStatus = $derived(
|
let showActivityStatus = $derived(
|
||||||
album.albumUsers.length > 0 && !$showAssetViewer && (album.isActivityEnabled || activityManager.commentCount > 0),
|
album.albumUsers.length > 0 &&
|
||||||
|
!assetManager.showAssetViewer &&
|
||||||
|
(album.isActivityEnabled || activityManager.commentCount > 0),
|
||||||
);
|
);
|
||||||
let isEditor = $derived(
|
let isEditor = $derived(
|
||||||
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
|
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
|
||||||
@@ -449,6 +460,7 @@
|
|||||||
{album}
|
{album}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
assetInteraction={currentAssetIntersection}
|
assetInteraction={currentAssetIntersection}
|
||||||
|
{assetManager}
|
||||||
{isShared}
|
{isShared}
|
||||||
{isSelectionMode}
|
{isSelectionMode}
|
||||||
{singleSelect}
|
{singleSelect}
|
||||||
@@ -626,7 +638,7 @@
|
|||||||
onclick={async () => {
|
onclick={async () => {
|
||||||
timelineManager.suspendTransitions = true;
|
timelineManager.suspendTransitions = true;
|
||||||
viewMode = AlbumPageViewMode.SELECT_ASSETS;
|
viewMode = AlbumPageViewMode.SELECT_ASSETS;
|
||||||
oldAt = { at: $gridScrollTarget?.at };
|
oldAt = { at: assetManager.gridScrollTarget?.at };
|
||||||
await navigate(
|
await navigate(
|
||||||
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } },
|
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } },
|
||||||
{ replaceState: true },
|
{ replaceState: true },
|
||||||
@@ -648,7 +660,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if $featureFlags.loaded && $featureFlags.map}
|
{#if $featureFlags.loaded && $featureFlags.map}
|
||||||
<AlbumMap {album} />
|
<AlbumMap {assetManager} {album} />
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if album.assetCount > 0}
|
{#if album.assetCount > 0}
|
||||||
@@ -735,7 +747,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{#if album.albumUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer}
|
{#if album.albumUsers.length > 0 && album && isShowActivity && $user && !assetManager.showAssetViewer}
|
||||||
<div class="flex">
|
<div class="flex">
|
||||||
<div
|
<div
|
||||||
transition:fly={{ duration: 150 }}
|
transition:fly={{ duration: 150 }}
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getAlbumInfo } from '@immich/sdk';
|
import { getAlbumInfo } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const [album, asset] = await Promise.all([
|
const album = await getAlbumInfo({ id: params.albumId, withoutAssets: true });
|
||||||
getAlbumInfo({ id: params.albumId, withoutAssets: true }),
|
|
||||||
getAssetInfoFromParam(params),
|
|
||||||
]);
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
album,
|
album,
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: album.albumName,
|
title: album.albumName,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -14,6 +14,7 @@
|
|||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
|
|
||||||
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
|
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
@@ -33,6 +34,15 @@
|
|||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
@@ -51,6 +61,7 @@
|
|||||||
enableRouting={true}
|
enableRouting={true}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
removeAction={AssetAction.UNARCHIVE}
|
removeAction={AssetAction.UNARCHIVE}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('archive'),
|
title: $t('archive'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
@@ -37,6 +38,15 @@
|
|||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
@@ -56,6 +66,7 @@
|
|||||||
withStacked={true}
|
withStacked={true}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
removeAction={AssetAction.UNFAVORITE}
|
removeAction={AssetAction.UNFAVORITE}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('favorites'),
|
title: $t('favorites'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -21,8 +21,8 @@
|
|||||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||||
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
|
||||||
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
import type { Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
@@ -32,6 +32,8 @@
|
|||||||
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@@ -43,6 +45,15 @@
|
|||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const handleNavigateToFolder = (folderName: string) => navigateToView(joinPaths(data.tree.path, folderName));
|
const handleNavigateToFolder = (folderName: string) => navigateToView(joinPaths(data.tree.path, folderName));
|
||||||
|
|
||||||
function getLinkForPath(path: string) {
|
function getLinkForPath(path: string) {
|
||||||
@@ -106,6 +117,7 @@
|
|||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
assets={data.pathAssets}
|
assets={data.pathAssets}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
{viewport}
|
{viewport}
|
||||||
showAssetName={true}
|
showAssetName={true}
|
||||||
pageHeaderOffset={54}
|
pageHeaderOffset={54}
|
||||||
|
|||||||
@@ -2,12 +2,11 @@ import { QueryParameter } from '$lib/constants';
|
|||||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const [, asset, $t] = await Promise.all([foldersStore.fetchTree(), getAssetInfoFromParam(params), getFormatter()]);
|
const [, $t] = await Promise.all([foldersStore.fetchTree(), getFormatter()]);
|
||||||
|
|
||||||
let tree = foldersStore.folders!;
|
let tree = foldersStore.folders!;
|
||||||
const path = url.searchParams.get(QueryParameter.PATH);
|
const path = url.searchParams.get(QueryParameter.PATH);
|
||||||
@@ -23,7 +22,7 @@ export const load = (async ({ params, url }) => {
|
|||||||
const pathAssets = tree.hasAssets ? await foldersStore.fetchAssetsByPath(tree.path) : null;
|
const pathAssets = tree.hasAssets ? await foldersStore.fetchAssetsByPath(tree.path) : null;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
tree,
|
tree,
|
||||||
pathAssets,
|
pathAssets,
|
||||||
meta: {
|
meta: {
|
||||||
|
|||||||
@@ -12,6 +12,7 @@
|
|||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
|
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
|
||||||
@@ -33,6 +34,15 @@
|
|||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
@@ -62,6 +72,7 @@
|
|||||||
enableRouting={true}
|
enableRouting={true}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
removeAction={AssetAction.SET_VISIBILITY_TIMELINE}
|
removeAction={AssetAction.SET_VISIBILITY_TIMELINE}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getAuthStatus } from '@immich/sdk';
|
import { getAuthStatus } from '@immich/sdk';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
@@ -14,11 +13,10 @@ export const load = (async ({ params, url }) => {
|
|||||||
redirect(302, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`);
|
redirect(302, `${AppRoute.AUTH_PIN_PROMPT}?continue=${encodeURIComponent(url.pathname + url.search)}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('locked_folder'),
|
title: $t('locked_folder'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,12 +1,10 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { run } from 'svelte/legacy';
|
|
||||||
|
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
|
||||||
import Map from '$lib/components/shared-components/map/map.svelte';
|
import Map from '$lib/components/shared-components/map/map.svelte';
|
||||||
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
import Portal from '$lib/components/shared-components/portal/portal.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
@@ -19,16 +17,23 @@
|
|||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
|
|
||||||
|
|
||||||
let viewingAssets: string[] = $state([]);
|
let viewingAssets: string[] = $state([]);
|
||||||
let viewingAssetCursor = 0;
|
let viewingAssetCursor = 0;
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
onDestroy(() => {
|
onDestroy(() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetManager.showAssetViewer = false;
|
||||||
});
|
});
|
||||||
|
|
||||||
run(() => {
|
$effect(() => {
|
||||||
if (!$featureFlags.map) {
|
if (!$featureFlags.map) {
|
||||||
handlePromiseError(goto(AppRoute.PHOTOS));
|
handlePromiseError(goto(AppRoute.PHOTOS));
|
||||||
}
|
}
|
||||||
@@ -37,13 +42,12 @@
|
|||||||
async function onViewAssets(assetIds: string[]) {
|
async function onViewAssets(assetIds: string[]) {
|
||||||
viewingAssets = assetIds;
|
viewingAssets = assetIds;
|
||||||
viewingAssetCursor = 0;
|
viewingAssetCursor = 0;
|
||||||
await setAssetId(assetIds[0]);
|
await navigate({ targetRoute: 'current', assetId: assetIds[0] });
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateNext() {
|
async function navigateNext() {
|
||||||
if (viewingAssetCursor < viewingAssets.length - 1) {
|
if (viewingAssetCursor < viewingAssets.length - 1) {
|
||||||
await setAssetId(viewingAssets[++viewingAssetCursor]);
|
await navigate({ targetRoute: 'current', assetId: viewingAssets[++viewingAssetCursor] });
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
@@ -51,21 +55,19 @@
|
|||||||
|
|
||||||
async function navigatePrevious() {
|
async function navigatePrevious() {
|
||||||
if (viewingAssetCursor > 0) {
|
if (viewingAssetCursor > 0) {
|
||||||
await setAssetId(viewingAssets[--viewingAssetCursor]);
|
await navigate({ targetRoute: 'current', assetId: viewingAssets[--viewingAssetCursor] });
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function navigateRandom() {
|
async function navigateRandom() {
|
||||||
if (viewingAssets.length <= 0) {
|
if (viewingAssets.length > 0) {
|
||||||
return undefined;
|
const index = Math.floor(Math.random() * viewingAssets.length);
|
||||||
|
await navigate({ targetRoute: 'current', assetId: viewingAssets[index] });
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
const index = Math.floor(Math.random() * viewingAssets.length);
|
return false;
|
||||||
const asset = await setAssetId(viewingAssets[index]);
|
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
|
||||||
return asset;
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -76,16 +78,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</UserPageLayout>
|
</UserPageLayout>
|
||||||
<Portal target="body">
|
<Portal target="body">
|
||||||
{#if $showAssetViewer}
|
{#if assetManager.showAssetViewer}
|
||||||
{#await import('../../../../../lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
{#await import('../../../../../lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
|
||||||
<AssetViewer
|
<AssetViewer
|
||||||
asset={$viewingAsset}
|
{assetManager}
|
||||||
showNavigation={viewingAssets.length > 1}
|
showNavigation={viewingAssets.length > 1}
|
||||||
onNext={navigateNext}
|
onNext={navigateNext}
|
||||||
onPrevious={navigatePrevious}
|
onPrevious={navigatePrevious}
|
||||||
onRandom={navigateRandom}
|
onRandom={navigateRandom}
|
||||||
onClose={() => {
|
onClose={() => {
|
||||||
assetViewingStore.showAssetViewer(false);
|
assetManager.showAssetViewer = false;
|
||||||
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
|
||||||
}}
|
}}
|
||||||
isShared={false}
|
isShared={false}
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('map'),
|
title: $t('map'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,5 +1,23 @@
|
|||||||
<script>
|
<script lang="ts">
|
||||||
import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte';
|
import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: PageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<MemoryViewer />
|
<MemoryViewer {assetManager} />
|
||||||
|
|||||||
@@ -1,16 +1,14 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
const user = await authenticate(url);
|
const user = await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('memory'),
|
title: $t('memory'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { AssetVisibility } from '@immich/sdk';
|
import { AssetVisibility } from '@immich/sdk';
|
||||||
@@ -34,6 +35,15 @@
|
|||||||
onDestroy(() => timelineManager.destroy());
|
onDestroy(() => timelineManager.destroy());
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
assetInteraction.clearMultiselect();
|
assetInteraction.clearMultiselect();
|
||||||
@@ -43,7 +53,7 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
<main class="relative h-dvh overflow-hidden px-2 md:px-6 max-md:pt-(--navbar-height-md) pt-(--navbar-height)">
|
||||||
<AssetGrid enableRouting={true} {timelineManager} {assetInteraction} onEscape={handleEscape} />
|
<AssetGrid enableRouting={true} {timelineManager} {assetInteraction} {assetManager} onEscape={handleEscape} />
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
{#if assetInteraction.selectionActive}
|
{#if assetInteraction.selectionActive}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getUser } from '@immich/sdk';
|
import { getUser } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
@@ -8,11 +7,10 @@ export const load = (async ({ params, url }) => {
|
|||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
|
|
||||||
const partner = await getUser({ id: params.userId });
|
const partner = await getUser({ id: params.userId });
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
partner,
|
partner,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('partner'),
|
title: $t('partner'),
|
||||||
|
|||||||
@@ -31,13 +31,13 @@
|
|||||||
notificationController,
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte';
|
import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte';
|
||||||
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { websocketEvents } from '$lib/stores/websocket';
|
import { websocketEvents } from '$lib/stores/websocket';
|
||||||
@@ -75,7 +75,6 @@
|
|||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let numberOfAssets = $state(data.statistics.assets);
|
let numberOfAssets = $state(data.statistics.assets);
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
|
||||||
|
|
||||||
const timelineManager = new TimelineManager();
|
const timelineManager = new TimelineManager();
|
||||||
$effect(() => void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id }));
|
$effect(() => void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id }));
|
||||||
@@ -83,6 +82,15 @@
|
|||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
|
let viewMode: PersonPageViewMode = $state(PersonPageViewMode.VIEW_ASSETS);
|
||||||
let isEditingName = $state(false);
|
let isEditingName = $state(false);
|
||||||
let previousRoute: string = $state(AppRoute.EXPLORE);
|
let previousRoute: string = $state(AppRoute.EXPLORE);
|
||||||
@@ -123,7 +131,7 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const handleEscape = async () => {
|
const handleEscape = async () => {
|
||||||
if ($showAssetViewer) {
|
if (assetManager.showAssetViewer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
@@ -388,6 +396,7 @@
|
|||||||
{person}
|
{person}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}
|
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}
|
||||||
singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON}
|
singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON}
|
||||||
onSelect={handleSelectFeaturePhoto}
|
onSelect={handleSelectFeaturePhoto}
|
||||||
|
|||||||
@@ -1,23 +1,21 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getPerson, getPersonStatistics } from '@immich/sdk';
|
import { getPerson, getPersonStatistics } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
|
|
||||||
const [person, statistics, asset] = await Promise.all([
|
const [person, statistics] = await Promise.all([
|
||||||
getPerson({ id: params.personId }),
|
getPerson({ id: params.personId }),
|
||||||
getPersonStatistics({ id: params.personId }),
|
getPersonStatistics({ id: params.personId }),
|
||||||
getAssetInfoFromParam(params),
|
|
||||||
]);
|
]);
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
person,
|
person,
|
||||||
statistics,
|
statistics,
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: person.name || $t('person'),
|
title: person.name || $t('person'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -22,9 +22,9 @@
|
|||||||
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
|
||||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import {
|
import {
|
||||||
@@ -39,12 +39,27 @@
|
|||||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: PageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
|
||||||
const timelineManager = new TimelineManager();
|
const timelineManager = new TimelineManager();
|
||||||
void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true });
|
void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true });
|
||||||
onDestroy(() => timelineManager.destroy());
|
onDestroy(() => timelineManager.destroy());
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
let selectedAssets = $derived(assetInteraction.selectedAssets);
|
let selectedAssets = $derived(assetInteraction.selectedAssets);
|
||||||
@@ -59,7 +74,7 @@
|
|||||||
return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
|
return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
|
||||||
});
|
});
|
||||||
const handleEscape = () => {
|
const handleEscape = () => {
|
||||||
if ($showAssetViewer) {
|
if (assetManager.showAssetViewer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
@@ -93,6 +108,7 @@
|
|||||||
enableRouting={true}
|
enableRouting={true}
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
removeAction={AssetAction.ARCHIVE}
|
removeAction={AssetAction.ARCHIVE}
|
||||||
onEscape={handleEscape}
|
onEscape={handleEscape}
|
||||||
withStacked
|
withStacked
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('photos'),
|
title: $t('photos'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -23,10 +23,10 @@
|
|||||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||||
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
|
||||||
import { lang, locale } from '$lib/stores/preferences.store';
|
import { lang, locale } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
@@ -47,11 +47,17 @@
|
|||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||||
import { tick } from 'svelte';
|
import { onDestroy, tick } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
data: PageData;
|
||||||
|
}
|
||||||
|
|
||||||
|
let { data }: Props = $props();
|
||||||
|
|
||||||
const MAX_ASSET_COUNT = 5000;
|
const MAX_ASSET_COUNT = 5000;
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
|
||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
|
|
||||||
// The GalleryViewer pushes it's own history state, which causes weird
|
// The GalleryViewer pushes it's own history state, which causes weird
|
||||||
@@ -83,8 +89,17 @@
|
|||||||
|
|
||||||
let timelineManager = new TimelineManager();
|
let timelineManager = new TimelineManager();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const onEscape = () => {
|
const onEscape = () => {
|
||||||
if ($showAssetViewer) {
|
if (assetManager.showAssetViewer) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -379,6 +394,7 @@
|
|||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
assets={searchResultAssets}
|
assets={searchResultAssets}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
onIntersected={loadNextPage}
|
onIntersected={loadNextPage}
|
||||||
showArchiveIcon={true}
|
showArchiveIcon={true}
|
||||||
{viewport}
|
{viewport}
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('search'),
|
title: $t('search'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,14 +5,14 @@
|
|||||||
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||||
import PasswordField from '$lib/components/shared-components/password-field.svelte';
|
import PasswordField from '$lib/components/shared-components/password-field.svelte';
|
||||||
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { user } from '$lib/stores/user.store';
|
import { user } from '$lib/stores/user.store';
|
||||||
import { setSharedLink } from '$lib/utils';
|
import { setSharedLink } from '$lib/utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { getMySharedLink, SharedLinkType } from '@immich/sdk';
|
import { getMySharedLink, SharedLinkType } from '@immich/sdk';
|
||||||
import { Button } from '@immich/ui';
|
import { Button } from '@immich/ui';
|
||||||
import { tick } from 'svelte';
|
import { onDestroy, tick } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
|
||||||
@@ -22,12 +22,20 @@
|
|||||||
|
|
||||||
let { data }: Props = $props();
|
let { data }: Props = $props();
|
||||||
|
|
||||||
let { gridScrollTarget } = assetViewingStore;
|
|
||||||
let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = $state(data);
|
let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = $state(data);
|
||||||
let { title, description } = $state(meta);
|
let { title, description } = $state(meta);
|
||||||
let isOwned = $derived($user ? $user.id === sharedLink?.userId : false);
|
let isOwned = $derived($user ? $user.id === sharedLink?.userId : false);
|
||||||
let password = $state('');
|
let password = $state('');
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const handlePasswordSubmit = async () => {
|
const handlePasswordSubmit = async () => {
|
||||||
try {
|
try {
|
||||||
sharedLink = await getMySharedLink({ password, key });
|
sharedLink = await getMySharedLink({ password, key });
|
||||||
@@ -39,7 +47,7 @@
|
|||||||
$t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } });
|
$t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } });
|
||||||
await tick();
|
await tick();
|
||||||
await navigate(
|
await navigate(
|
||||||
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget },
|
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: assetManager.gridScrollTarget },
|
||||||
{ forceNavigate: true, replaceState: true },
|
{ forceNavigate: true, replaceState: true },
|
||||||
);
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -88,10 +96,10 @@
|
|||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
|
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
|
||||||
<AlbumViewer {sharedLink} />
|
<AlbumViewer {assetManager} {sharedLink} />
|
||||||
{/if}
|
{/if}
|
||||||
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
|
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
|
||||||
<div class="immich-scrollbar">
|
<div class="immich-scrollbar">
|
||||||
<IndividualSharedViewer {sharedLink} {isOwned} />
|
<IndividualSharedViewer {assetManager} {sharedLink} {isOwned} />
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils';
|
import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getMySharedLink, isHttpError } from '@immich/sdk';
|
import { getMySharedLink, isHttpError } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
@@ -12,7 +11,7 @@ export const load = (async ({ params, url }) => {
|
|||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const [sharedLink, asset] = await Promise.all([getMySharedLink({ key }), getAssetInfoFromParam(params)]);
|
const sharedLink = await getMySharedLink({ key });
|
||||||
setSharedLink(sharedLink);
|
setSharedLink(sharedLink);
|
||||||
const assetCount = sharedLink.assets.length;
|
const assetCount = sharedLink.assets.length;
|
||||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||||
@@ -21,7 +20,7 @@ export const load = (async ({ params, url }) => {
|
|||||||
return {
|
return {
|
||||||
sharedLink,
|
sharedLink,
|
||||||
sharedLinkKey: key,
|
sharedLinkKey: key,
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'),
|
title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'),
|
||||||
description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }),
|
description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }),
|
||||||
|
|||||||
@@ -20,6 +20,7 @@
|
|||||||
import { onDestroy } from 'svelte';
|
import { onDestroy } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@@ -33,6 +34,15 @@
|
|||||||
$effect(() => void timelineManager.updateOptions({ deferInit: !tag, tagId: tag?.id }));
|
$effect(() => void timelineManager.updateOptions({ deferInit: !tag, tagId: tag?.id }));
|
||||||
onDestroy(() => timelineManager.destroy());
|
onDestroy(() => timelineManager.destroy());
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
let tags = $derived<TagResponseDto[]>(data.tags);
|
let tags = $derived<TagResponseDto[]>(data.tags);
|
||||||
const tree = $derived(TreeNode.fromTags(tags));
|
const tree = $derived(TreeNode.fromTags(tags));
|
||||||
const tag = $derived(tree.traverse(data.path));
|
const tag = $derived(tree.traverse(data.path));
|
||||||
@@ -118,7 +128,13 @@
|
|||||||
|
|
||||||
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
|
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
|
||||||
{#if tag.hasAssets}
|
{#if tag.hasAssets}
|
||||||
<AssetGrid enableRouting={true} {timelineManager} {assetInteraction} removeAction={AssetAction.UNARCHIVE}>
|
<AssetGrid
|
||||||
|
enableRouting={true}
|
||||||
|
{timelineManager}
|
||||||
|
{assetInteraction}
|
||||||
|
{assetManager}
|
||||||
|
removeAction={AssetAction.UNARCHIVE}
|
||||||
|
>
|
||||||
{#snippet empty()}
|
{#snippet empty()}
|
||||||
<TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} />
|
<TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|||||||
@@ -1,13 +1,11 @@
|
|||||||
import { QueryParameter } from '$lib/constants';
|
import { QueryParameter } from '$lib/constants';
|
||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getAllTags } from '@immich/sdk';
|
import { getAllTags } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
const tags = await getAllTags();
|
const tags = await getAllTags();
|
||||||
@@ -15,7 +13,7 @@ export const load = (async ({ params, url }) => {
|
|||||||
return {
|
return {
|
||||||
path: url.searchParams.get(QueryParameter.PATH) ?? '',
|
path: url.searchParams.get(QueryParameter.PATH) ?? '',
|
||||||
tags,
|
tags,
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('tags'),
|
title: $t('tags'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
notificationController,
|
notificationController,
|
||||||
} from '$lib/components/shared-components/notification/notification';
|
} from '$lib/components/shared-components/notification/notification';
|
||||||
import { AppRoute } from '$lib/constants';
|
import { AppRoute } from '$lib/constants';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
import { modalManager } from '$lib/managers/modal-manager.svelte';
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
@@ -42,6 +43,15 @@
|
|||||||
|
|
||||||
const assetInteraction = new AssetInteraction();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const handleEmptyTrash = async () => {
|
const handleEmptyTrash = async () => {
|
||||||
const isConfirmed = await modalManager.showDialog({ prompt: $t('empty_trash_confirmation') });
|
const isConfirmed = await modalManager.showDialog({ prompt: $t('empty_trash_confirmation') });
|
||||||
if (!isConfirmed) {
|
if (!isConfirmed) {
|
||||||
@@ -117,7 +127,7 @@
|
|||||||
</HStack>
|
</HStack>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
<AssetGrid enableRouting={true} {timelineManager} {assetInteraction} onEscape={handleEscape}>
|
<AssetGrid enableRouting={true} {timelineManager} {assetInteraction} {assetManager} onEscape={handleEscape}>
|
||||||
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4">
|
<p class="font-medium text-gray-500/60 dark:text-gray-300/60 p-4">
|
||||||
{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}
|
{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('trash'),
|
title: $t('trash'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('utilities'),
|
title: $t('utilities'),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -19,6 +19,8 @@
|
|||||||
import { mdiCheckOutline, mdiInformationOutline, mdiKeyboard, mdiTrashCanOutline } from '@mdi/js';
|
import { mdiCheckOutline, mdiInformationOutline, mdiKeyboard, mdiTrashCanOutline } from '@mdi/js';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import type { PageData } from './$types';
|
import type { PageData } from './$types';
|
||||||
|
import { onDestroy } from 'svelte';
|
||||||
|
import { AssetManager } from '$lib/managers/asset-manager/asset-manager.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
data: PageData;
|
data: PageData;
|
||||||
@@ -36,6 +38,15 @@
|
|||||||
info?: string;
|
info?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const assetManager = new AssetManager();
|
||||||
|
$effect(() => {
|
||||||
|
if (data.assetId) {
|
||||||
|
assetManager.showAssetViewer = true;
|
||||||
|
void assetManager.updateOptions({ assetId: data.assetId });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
onDestroy(() => assetManager.destroy());
|
||||||
|
|
||||||
const duplicateShortcuts: Shortcuts = {
|
const duplicateShortcuts: Shortcuts = {
|
||||||
general: [],
|
general: [],
|
||||||
actions: [
|
actions: [
|
||||||
@@ -207,6 +218,7 @@
|
|||||||
{#key duplicates[0].duplicateId}
|
{#key duplicates[0].duplicateId}
|
||||||
<DuplicatesCompareControl
|
<DuplicatesCompareControl
|
||||||
assets={duplicates[0].assets}
|
assets={duplicates[0].assets}
|
||||||
|
{assetManager}
|
||||||
onResolve={(duplicateAssetIds, trashIds) =>
|
onResolve={(duplicateAssetIds, trashIds) =>
|
||||||
handleResolve(duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
|
handleResolve(duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
|
||||||
onStack={(assets) => handleStack(duplicates[0].duplicateId, assets)}
|
onStack={(assets) => handleStack(duplicates[0].duplicateId, assets)}
|
||||||
|
|||||||
@@ -1,17 +1,15 @@
|
|||||||
import { authenticate } from '$lib/utils/auth';
|
import { authenticate } from '$lib/utils/auth';
|
||||||
import { getFormatter } from '$lib/utils/i18n';
|
import { getFormatter } from '$lib/utils/i18n';
|
||||||
import { getAssetInfoFromParam } from '$lib/utils/navigation';
|
|
||||||
import { getAssetDuplicates } from '@immich/sdk';
|
import { getAssetDuplicates } from '@immich/sdk';
|
||||||
import type { PageLoad } from './$types';
|
import type { PageLoad } from './$types';
|
||||||
|
|
||||||
export const load = (async ({ params, url }) => {
|
export const load = (async ({ params, url }) => {
|
||||||
await authenticate(url);
|
await authenticate(url);
|
||||||
const asset = await getAssetInfoFromParam(params);
|
|
||||||
const duplicates = await getAssetDuplicates();
|
const duplicates = await getAssetDuplicates();
|
||||||
const $t = await getFormatter();
|
const $t = await getFormatter();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
asset,
|
assetId: params.assetId,
|
||||||
duplicates,
|
duplicates,
|
||||||
meta: {
|
meta: {
|
||||||
title: $t('duplicates'),
|
title: $t('duplicates'),
|
||||||
|
|||||||
Reference in New Issue
Block a user