refactor: asset manager

This commit is contained in:
wuzihao051119
2025-06-29 04:20:20 +08:00
parent 09cbc5d3f4
commit 769d0aed87
49 changed files with 556 additions and 354 deletions

View File

@@ -1,7 +1,8 @@
<script lang="ts">
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.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 { IconButton } from '@immich/ui';
import { mdiMapOutline } from '@mdi/js';
@@ -9,12 +10,12 @@
import { t } from 'svelte-i18n';
interface Props {
assetManager: AssetManager;
album: AlbumResponseDto;
}
let { album }: Props = $props();
let { assetManager = $bindable(), album }: Props = $props();
let abortController: AbortController;
let { setAssetId } = assetViewingStore;
let mapMarkers: MapMarkerResponseDto[] = $state([]);
@@ -24,7 +25,7 @@
onDestroy(() => {
abortController?.abort();
assetViewingStore.showAssetViewer(false);
assetManager.showAssetViewer = false;
});
async function loadMapMarkers() {
@@ -56,7 +57,7 @@
const assetIds = await modalManager.show(MapModal, { mapMarkers });
if (assetIds) {
await setAssetId(assetIds[0]);
await navigate({ targetRoute: 'current', assetId: assetIds[0] });
}
}
</script>

View File

@@ -4,9 +4,9 @@
import AlbumMap from '$lib/components/album-page/album-map.svelte';
import SelectAllAssets from '$lib/components/photos-page/actions/select-all-assets.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.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 { featureFlags } from '$lib/stores/server-config.store';
import { handlePromiseError } from '$lib/utils';
@@ -25,16 +25,15 @@
import AlbumSummary from './album-summary.svelte';
interface Props {
assetManager: AssetManager;
sharedLink: SharedLinkResponseDto;
user?: UserResponseDto | undefined;
}
let { sharedLink, user = undefined }: Props = $props();
let { assetManager = $bindable(), sharedLink, user = undefined }: Props = $props();
const album = sharedLink.album as AlbumResponseDto;
let { isViewing: showAssetViewer } = assetViewingStore;
const timelineManager = new TimelineManager();
$effect(() => void timelineManager.updateOptions({ albumId: album.id, order: album.order }));
onDestroy(() => timelineManager.destroy());
@@ -53,7 +52,7 @@
use:shortcut={{
shortcut: { key: 'Escape' },
onShortcut: () => {
if (!$showAssetViewer && assetInteraction.selectionActive) {
if (!assetManager.showAssetViewer && assetInteraction.selectionActive) {
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)">
<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">
<!-- ALBUM TITLE -->
<h1
@@ -129,7 +128,7 @@
/>
{/if}
{#if sharedLink.showMetadata && $featureFlags.loaded && $featureFlags.map}
<AlbumMap {album} />
<AlbumMap {assetManager} {album} />
{/if}
<ThemeButton />
{/snippet}

View File

@@ -1,22 +1,20 @@
<script lang="ts">
import { shortcut } from '$lib/actions/shortcut';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { downloadFile } from '$lib/utils/asset-utils';
import { getAssetInfo } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiFolderDownloadOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
interface Props {
asset: TimelineAsset;
assetManager: AssetManager;
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>
<svelte:document use:shortcut={{ shortcut: { key: 'd', shift: true }, onShortcut: onDownloadFile }} />

View File

@@ -21,6 +21,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import MenuOption from '$lib/components/shared-components/context-menu/menu-option.svelte';
import { AppRoute } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { user } from '$lib/stores/user.store';
import { photoZoomState } from '$lib/stores/zoom-image.store';
import { getAssetJobName, getSharedLink } from '$lib/utils';
@@ -32,7 +33,6 @@
AssetTypeEnum,
AssetVisibility,
type AlbumResponseDto,
type AssetResponseDto,
type PersonResponseDto,
type StackResponseDto,
} from '@immich/sdk';
@@ -55,7 +55,7 @@
import { t } from 'svelte-i18n';
interface Props {
asset: AssetResponseDto;
assetManager: AssetManager;
album?: AlbumResponseDto | null;
person?: PersonResponseDto | null;
stack?: StackResponseDto | null;
@@ -75,7 +75,7 @@
}
let {
asset,
assetManager = $bindable(),
album = null,
person = null,
stack = null,
@@ -93,6 +93,8 @@
motionPhoto,
}: Props = $props();
let asset = $derived(assetManager.asset);
const sharedLink = getSharedLink();
let isOwner = $derived($user && asset.ownerId === $user?.id);
let showDownloadButton = $derived(sharedLink ? sharedLink.allowDownload : !asset.isOffline);
@@ -158,7 +160,7 @@
{/if}
{#if !isOwner && showDownloadButton}
<DownloadAction asset={toTimelineAsset(asset)} />
<DownloadAction {assetManager} />
{/if}
{#if showDetailButton}
@@ -177,7 +179,7 @@
<MenuOption icon={mdiPresentationPlay} text={$t('slideshow')} onClick={onPlaySlideshow} />
{/if}
{#if showDownloadButton}
<DownloadAction asset={toTimelineAsset(asset)} menuItem />
<DownloadAction {assetManager} menuItem />
{/if}
{#if !isLocked}

View File

@@ -7,22 +7,21 @@
import AssetViewerNavBar from '$lib/components/asset-viewer/asset-viewer-nav-bar.svelte';
import { AssetAction, ProjectionType } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { 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 { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isShowDetail } from '$lib/stores/preferences.store';
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { user } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
import { getAssetJobMessage, getSharedLink, handlePromiseError } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { navigate } from '$lib/utils/navigation';
import { SlideshowHistory } from '$lib/utils/slideshow-history';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import {
AssetJobName,
AssetTypeEnum,
getAllAlbums,
getStack,
runAssetJobs,
type AlbumResponseDto,
@@ -48,8 +47,7 @@
type HasAsset = boolean;
interface Props {
asset: AssetResponseDto;
preloadAssets?: TimelineAsset[];
assetManager: AssetManager;
showNavigation?: boolean;
withStacked?: boolean;
isShared?: boolean;
@@ -61,13 +59,12 @@
onClose: (asset: AssetResponseDto) => void;
onNext: () => Promise<HasAsset>;
onPrevious: () => Promise<HasAsset>;
onRandom: () => Promise<{ id: string } | undefined>;
onRandom: () => Promise<HasAsset>;
copyImage?: () => Promise<void>;
}
let {
asset = $bindable(),
preloadAssets = $bindable([]),
assetManager = $bindable(),
showNavigation = true,
withStacked = false,
isShared = false,
@@ -83,7 +80,6 @@
copyImage = $bindable(),
}: Props = $props();
const { setAssetId } = assetViewingStore;
const {
restartProgress: restartSlideshowProgress,
stopProgress: stopSlideshowProgress,
@@ -94,10 +90,13 @@
const stackThumbnailSize = 60;
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 sharedLink = getSharedLink();
let enableDetailPanel = asset.hasMetadata;
let enableDetailPanel = $derived(asset.hasMetadata);
let slideshowStateUnsubscribe: () => void;
let shuffleSlideshowUnsubscribe: () => void;
let previewStackedAsset: AssetResponseDto | undefined = $state();
@@ -146,7 +145,7 @@
}
};
onMount(async () => {
onMount(() => {
unsubscribes.push(
websocketEvents.on('on_upload_success', (asset) => onAssetUpdate({ event: 'upload', asset })),
websocketEvents.on('on_asset_update', (asset) => onAssetUpdate({ event: 'update', asset })),
@@ -169,9 +168,7 @@
}
});
if (!sharedLink) {
await handleGetAllAlbums();
}
// TODO: empty shared link returns 404.
});
onDestroy(() => {
@@ -190,18 +187,6 @@
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 = () => {
if ($isShowDetail) {
$isShowDetail = false;
@@ -238,11 +223,11 @@
let hasNext = false;
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) {
const asset = await onRandom();
if (asset) {
slideshowHistory.queue(asset);
await onRandom();
if (assetManager.asset) {
slideshowHistory.queue(assetManager.asset);
hasNext = true;
}
}
@@ -281,8 +266,9 @@
let assetViewerHtmlElement = $state<HTMLElement>();
const slideshowHistory = new SlideshowHistory((asset) => {
handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true)));
const slideshowHistory = new SlideshowHistory(async (asset) => {
await navigate({ targetRoute: 'current', assetId: asset.id });
$restartSlideshowProgress = true;
});
const handleVideoStarted = () => {
@@ -325,7 +311,7 @@
const handleAction = async (action: Action) => {
switch (action.type) {
case AssetAction.ADD_TO_ALBUM: {
await handleGetAllAlbums();
await assetManager.refreshAlbums();
break;
}
case AssetAction.SET_STACK_PRIMARY_ASSET: {
@@ -364,11 +350,6 @@
handlePromiseError(activityManager.init(album.id, asset.id));
}
});
$effect(() => {
if (asset.id) {
handlePromiseError(handleGetAllAlbums());
}
});
</script>
<svelte:document bind:fullscreenElement />
@@ -383,7 +364,7 @@
{#if $slideshowState === SlideshowState.None && !isShowEditor}
<div class="col-span-4 col-start-1 row-span-1 row-start-1 transition-transform">
<AssetViewerNavBar
{asset}
{assetManager}
{album}
{person}
{stack}
@@ -529,7 +510,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"
translate="yes"
>
<DetailPanel {asset} currentAlbum={album} albums={appearsInAlbums} onClose={() => ($isShowDetail = false)} />
<DetailPanel {asset} currentAlbum={album} {albums} onClose={() => ($isShowDetail = false)} />
</div>
{/if}

View File

@@ -13,7 +13,7 @@
import { locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.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 { getByteUnitString } from '$lib/utils/byte-units';
import { handleError } from '$lib/utils/handle-error';
@@ -22,7 +22,6 @@
import { getParentPath } from '$lib/utils/tree-utils';
import {
AssetMediaSize,
getAssetInfo,
updateAsset,
type AlbumResponseDto,
type AssetResponseDto,
@@ -83,19 +82,6 @@
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(
(() => {
const lat = asset.exifInfo?.latitude;
@@ -127,11 +113,8 @@
return undefined;
};
const handleRefreshPeople = async () => {
await getAssetInfo({ id: asset.id }).then((data) => {
people = data?.people || [];
unassignedFaces = data?.unassignedFaces || [];
});
// TODO: refresh people
const handleRefreshPeople = () => {
showEditFaces = false;
};

View File

@@ -2,7 +2,6 @@
import ImageThumbnail from '$lib/components/assets/thumbnail/image-thumbnail.svelte';
import { notificationController } from '$lib/components/shared-components/notification/notification';
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 { getPeopleThumbnailUrl } from '$lib/utils';
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) {
handleError(error, 'Error tagging face');
} finally {

View File

@@ -3,7 +3,6 @@
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { timeBeforeShowLoadingSpinner } from '$lib/constants';
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 { boundingBoxesArray } from '$lib/stores/people.store';
import { websocketEvents } from '$lib/stores/websocket';
@@ -20,6 +19,7 @@
type AssetFaceResponseDto,
type PersonResponseDto,
} from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiAccountOff, mdiArrowLeftThin, mdiPencil, mdiRestart, mdiTrashCan } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@@ -28,7 +28,6 @@
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
import { IconButton } from '@immich/ui';
interface Props {
assetId: string;
@@ -184,7 +183,8 @@
peopleWithFaces = peopleWithFaces.filter((f) => f.id !== face.id);
await assetViewingStore.setAssetId(assetId);
// TODO: manual tag face
// await assetViewingStore.setAssetId(assetId);
} catch (error) {
handleError(error, $t('error_delete_face'));
}

View File

@@ -26,10 +26,10 @@
NotificationType,
} from '$lib/components/shared-components/notification/notification';
import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
import { preferences } from '$lib/stores/user.store';
@@ -61,6 +61,12 @@
import { t } from 'svelte-i18n';
import { Tween } from 'svelte/motion';
interface Props {
assetManager: AssetManager;
}
let { assetManager = $bindable() }: Props = $props();
let memoryGallery: HTMLElement | undefined = $state();
let memoryWrapper: HTMLElement | undefined = $state();
let galleryInView = $state(false);
@@ -76,7 +82,6 @@
let isSaved = $derived(current?.memory.isSaved);
let viewerHeight = $state(0);
const { isViewing } = assetViewingStore;
const viewport: Viewport = $state({ width: 0, height: 0 });
// need to include padding in the viewport for gallery
const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
@@ -85,7 +90,7 @@
let videoPlayer: HTMLVideoElement | undefined = $state();
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
const handleNavigate = async (asset?: { id: string }) => {
if ($isViewing) {
if (assetManager.showAssetViewer) {
return asset;
}
@@ -251,7 +256,7 @@
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
return;
}
if ($isViewing) {
if (assetManager.showAssetViewer) {
handlePromiseError(handleAction('initPlayer[AssetViewOpen]', 'pause'));
} else {
handlePromiseError(handleAction('initPlayer[AssetViewClosed]', 'reset'));
@@ -296,7 +301,7 @@
</script>
<svelte:document
use:shortcuts={$isViewing
use:shortcuts={assetManager.showAssetViewer
? []
: [
{ shortcut: { key: 'ArrowRight' }, onShortcut: () => handleNextAsset() },
@@ -640,6 +645,7 @@
bind:this={memoryGallery}
>
<GalleryViewer
{assetManager}
onNext={handleNextAsset}
onPrevious={handlePreviousAsset}
assets={currentTimelineAssets}

View File

@@ -12,7 +12,7 @@
import ChangeDate from '$lib/components/shared-components/change-date.svelte';
import Scrubber from '$lib/components/shared-components/scrubber/scrubber.svelte';
import { AppRoute, AssetAction } from '$lib/constants';
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
@@ -20,7 +20,6 @@
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
import ShortcutsModal from '$lib/modals/ShortcutsModal.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 { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
@@ -36,7 +35,7 @@
type ScrubberListener,
type TimelinePlainYearMonth,
} 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 { onMount, type Snippet } from 'svelte';
import type { UpdatePayload } from 'vite';
@@ -53,6 +52,7 @@
enableRouting: boolean;
timelineManager: TimelineManager;
assetInteraction: AssetInteraction;
assetManager: AssetManager;
removeAction?:
| AssetAction.UNARCHIVE
| AssetAction.ARCHIVE
@@ -78,6 +78,7 @@
enableRouting,
timelineManager = $bindable(),
assetInteraction,
assetManager = $bindable(),
removeAction = null,
withStacked = false,
showArchiveIcon = false,
@@ -91,8 +92,6 @@
empty,
}: Props = $props();
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
let element: HTMLElement | undefined = $state();
let timelineElement: HTMLElement | undefined = $state();
@@ -103,6 +102,8 @@
let scrubOverallPercent: number = $state(0);
let scrubberWidth = $state(0);
let asset = $derived(assetManager.asset);
// 60 is the bottom spacer element at 60px
let bottomSectionHeight = 60;
let leadout = $state(false);
@@ -177,7 +178,7 @@
};
const completeNav = async () => {
const scrollTarget = $gridScrollTarget?.at;
const scrollTarget = assetManager.gridScrollTarget?.at;
let scrolled = false;
if (scrollTarget) {
scrolled = await scrollToAssetId(scrollTarget);
@@ -212,9 +213,9 @@
setTimeout(() => {
const asset = $page.url.searchParams.get('at');
if (asset) {
$gridScrollTarget = { at: asset };
assetManager.gridScrollTarget = { at: asset };
void navigate(
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget },
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: assetManager.gridScrollTarget },
{ replaceState: true, forceNavigate: true },
);
} else {
@@ -438,12 +439,11 @@
};
const handlePrevious = async () => {
const laterAsset = await timelineManager.getLaterAsset($viewingAsset);
const laterAsset = await timelineManager.getLaterAsset(asset);
if (laterAsset) {
const preloadAsset = await timelineManager.getLaterAsset(laterAsset);
const asset = await getAssetInfo({ id: laterAsset.id, key: authManager.key });
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
// TODO: If preloadAsset is undefined, throw an exception.
// assetManager.preloadAssets = [await timelineManager.getLaterAsset(laterAsset)];
await navigate({ targetRoute: 'current', assetId: laterAsset.id });
}
@@ -451,11 +451,10 @@
};
const handleNext = async () => {
const earlierAsset = await timelineManager.getEarlierAsset($viewingAsset);
const earlierAsset = await timelineManager.getEarlierAsset(asset);
if (earlierAsset) {
const preloadAsset = await timelineManager.getEarlierAsset(earlierAsset);
const asset = await getAssetInfo({ id: earlierAsset.id, key: authManager.key });
assetViewingStore.setAsset(asset, preloadAsset ? [preloadAsset] : []);
// assetManager.preloadAssets = [await timelineManager.getEarlierAsset(earlierAsset)];
await navigate({ targetRoute: 'current', assetId: earlierAsset.id });
}
@@ -466,18 +465,21 @@
const randomAsset = await timelineManager.getRandomAsset();
if (randomAsset) {
const asset = await getAssetInfo({ id: randomAsset.id, key: authManager.key });
assetViewingStore.setAsset(asset);
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
return asset;
}
return !!randomAsset;
};
const handleClose = async (asset: { id: string }) => {
assetViewingStore.showAssetViewer(false);
assetManager.showAssetViewer = false;
showSkeleton = true;
$gridScrollTarget = { at: asset.id };
await navigate({ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget });
assetManager.gridScrollTarget = { at: asset.id };
await navigate({
targetRoute: 'current',
assetId: null,
assetGridRouteSearchParams: assetManager.gridScrollTarget,
});
};
const handlePreAction = async (action: Action) => {
@@ -722,7 +724,7 @@
let shortcutList = $derived(
(() => {
if (searchStore.isSearchEnabled || $showAssetViewer) {
if (searchStore.isSearchEnabled || assetManager.showAssetViewer) {
return [];
}
@@ -775,8 +777,8 @@
});
$effect(() => {
if ($showAssetViewer) {
const { localDateTime } = getTimes($viewingAsset.fileCreatedAt, DateTime.local().offset / 60);
if (assetManager.showAssetViewer) {
const { localDateTime } = getTimes(asset.fileCreatedAt, DateTime.local().offset / 60);
void timelineManager.loadMonthGroup({ year: localDateTime.year, month: localDateTime.month });
}
});
@@ -923,12 +925,11 @@
</section>
<Portal target="body">
{#if $showAssetViewer}
{#if assetManager.showAssetViewer}
{#await import('../asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
<AssetViewer
{assetManager}
{withStacked}
asset={$viewingAsset}
preloadAssets={$preloadAssets}
{isShared}
{album}
{person}

View File

@@ -3,16 +3,18 @@
import type { Action } from '$lib/components/asset-viewer/actions/action';
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
import { AppRoute, AssetAction } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { 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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect, downloadArchive } from '$lib/utils/asset-utils';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
import { handleError } from '$lib/utils/handle-error';
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 { t } from 'svelte-i18n';
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
@@ -22,14 +24,14 @@
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { IconButton } from '@immich/ui';
interface Props {
assetManager: AssetManager;
sharedLink: SharedLinkResponseDto;
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 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>
<section>
@@ -142,19 +151,17 @@
</ControlAppBar>
{/if}
<section class="my-[160px] mx-4" bind:clientHeight={viewport.height} bind:clientWidth={viewport.width}>
<GalleryViewer {assets} {assetInteraction} {viewport} />
<GalleryViewer {assets} {assetInteraction} {assetManager} {viewport} />
</section>
{:else if assets.length === 1}
{#await getAssetInfo({ id: assets[0].id, key: authManager.key }) then asset}
<AssetViewer
{asset}
showCloseButton={false}
onAction={handleAction}
onPrevious={() => Promise.resolve(false)}
onNext={() => Promise.resolve(false)}
onRandom={() => Promise.resolve(undefined)}
onClose={() => {}}
/>
{/await}
<AssetViewer
{assetManager}
showCloseButton={false}
onAction={handleAction}
onPrevious={() => Promise.resolve(false)}
onNext={() => Promise.resolve(false)}
onRandom={() => Promise.resolve(false)}
onClose={() => {}}
/>
{/if}
</section>

View File

@@ -4,11 +4,11 @@
import type { Action } from '$lib/components/asset-viewer/actions/action';
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { AppRoute, AssetAction } from '$lib/constants';
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
import ShortcutsModal from '$lib/modals/ShortcutsModal.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 { featureFlags } from '$lib/stores/server-config.store';
import { handlePromiseError } from '$lib/utils';
@@ -29,6 +29,7 @@
interface Props {
assets: (TimelineAsset | AssetResponseDto)[];
assetInteraction: AssetInteraction;
assetManager: AssetManager;
disableAssetSelect?: boolean;
showArchiveIcon?: boolean;
viewport: Viewport;
@@ -46,6 +47,7 @@
let {
assets = $bindable(),
assetInteraction,
assetManager = $bindable(),
disableAssetSelect = false,
showArchiveIcon = false,
viewport,
@@ -60,8 +62,6 @@
pageHeaderOffset = 0,
}: Props = $props();
let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
let geometry: CommonJustifiedLayout | undefined = $state();
$effect(() => {
@@ -151,8 +151,7 @@
});
const viewAssetHandler = async (asset: TimelineAsset) => {
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
await setAssetId(assets[currentViewAssetIndex].id);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
await navigate({ targetRoute: 'current', assetId: assets[currentViewAssetIndex].id });
};
const selectAllAssets = () => {
@@ -292,7 +291,7 @@
const shortcutList = $derived(
(() => {
if ($isViewerOpen) {
if (assetManager.showAssetViewer) {
return [];
}
@@ -344,7 +343,7 @@
}
};
const handleRandom = async (): Promise<{ id: string } | undefined> => {
const handleRandom = async (): Promise<boolean> => {
try {
let asset: { id: string } | undefined;
if (onRandom) {
@@ -357,14 +356,14 @@
}
if (!asset) {
return;
return false;
}
await navigateToAsset(asset);
return asset;
return true;
} catch (error) {
handleError(error, $t('errors.cannot_navigate_next_asset'));
return;
return false;
}
};
@@ -395,9 +394,8 @@
};
const navigateToAsset = async (asset?: { id: string }) => {
if (asset && asset.id !== $viewingAsset.id) {
await setAssetId(asset.id);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
if (asset && asset.id !== assetManager.asset.id) {
await navigate({ targetRoute: 'current', assetId: asset.id });
}
};
@@ -415,7 +413,7 @@
} else if (currentViewAssetIndex === assets.length) {
await handlePrevious();
} else {
await setAssetId(assets[currentViewAssetIndex].id);
await navigate({ targetRoute: 'current', assetId: assets[currentViewAssetIndex].id });
}
break;
}
@@ -512,16 +510,16 @@
{/if}
<!-- Overlay Asset Viewer -->
{#if $isViewerOpen}
{#if assetManager.showAssetViewer}
<Portal target="body">
<AssetViewer
asset={$viewingAsset}
{assetManager}
onAction={handleAction}
onPrevious={handlePrevious}
onNext={handleNext}
onRandom={handleRandom}
onClose={() => {
assetViewingStore.showAssetViewer(false);
assetManager.showAssetViewer = false;
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}}
/>

View File

@@ -2,7 +2,7 @@
import { shortcuts } from '$lib/actions/shortcut';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import DuplicateAsset from '$lib/components/utilities-page/duplicates/duplicate-asset.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import type { AssetManager } from '$lib/managers/asset-manager.svelte';
import { handlePromiseError } from '$lib/utils';
import { suggestDuplicate } from '$lib/utils/duplicate-utils';
import { navigate } from '$lib/utils/navigation';
@@ -15,12 +15,12 @@
interface Props {
assets: AssetResponseDto[];
assetManager: AssetManager;
onResolve: (duplicateAssetIds: string[], trashIds: string[]) => void;
onStack: (assets: AssetResponseDto[]) => void;
}
let { assets, onResolve, onStack }: Props = $props();
const { isViewing: showAssetViewer, asset: viewingAsset, setAsset } = assetViewingStore;
let { assets, assetManager, onResolve, onStack }: Props = $props();
const getAssetIndex = (id: string) => assets.findIndex((asset) => asset.id === id);
// eslint-disable-next-line svelte/no-unnecessary-state-wrap
@@ -39,35 +39,34 @@
});
onDestroy(() => {
assetViewingStore.showAssetViewer(false);
assetManager.showAssetViewer = false;
});
const onNext = () => {
const index = getAssetIndex($viewingAsset.id) + 1;
const onNext = async () => {
const index = getAssetIndex(assetManager.asset.id) + 1;
if (index >= assets.length) {
return Promise.resolve(false);
return false;
}
setAsset(assets[index]);
return Promise.resolve(true);
await navigate({ targetRoute: 'current', assetId: assets[index].id });
return true;
};
const onPrevious = () => {
const index = getAssetIndex($viewingAsset.id) - 1;
const onPrevious = async () => {
const index = getAssetIndex(assetManager.asset.id) - 1;
if (index < 0) {
return Promise.resolve(false);
return false;
}
setAsset(assets[index]);
return Promise.resolve(true);
await navigate({ targetRoute: 'current', assetId: assets[index].id });
return true;
};
const onRandom = () => {
const onRandom = async () => {
if (assets.length <= 0) {
return Promise.resolve(undefined);
return false;
}
const index = Math.floor(Math.random() * assets.length);
const asset = assets[index];
setAsset(asset);
return Promise.resolve(asset);
await navigate({ targetRoute: 'current', assetId: assets[index].id });
return true;
};
const onSelectAsset = (asset: AssetResponseDto) => {
@@ -102,9 +101,7 @@
{ shortcut: { key: 'a' }, onShortcut: onSelectAll },
{
shortcut: { key: 's' },
onShortcut: () => {
setAsset(assets[0]);
},
onShortcut: () => navigate({ targetRoute: 'current', assetId: assets[0].id }),
},
{ shortcut: { key: 'd' }, onShortcut: onSelectNone },
{ shortcut: { key: 'c', shift: true }, onShortcut: handleResolve },
@@ -170,23 +167,23 @@
{asset}
{onSelectAsset}
isSelected={selectedAssetIds.has(asset.id)}
onViewAsset={(asset) => setAsset(asset)}
onViewAsset={(asset) => navigate({ targetRoute: 'current', assetId: asset.id })}
/>
{/each}
</div>
</div>
{#if $showAssetViewer}
{#if assetManager.showAssetViewer}
{#await import('$lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
<Portal target="body">
<AssetViewer
asset={$viewingAsset}
{assetManager}
showNavigation={assets.length > 1}
{onNext}
{onPrevious}
{onRandom}
onClose={() => {
assetViewingStore.showAssetViewer(false);
assetManager.showAssetViewer = false;
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}}
/>

View File

@@ -0,0 +1,143 @@
import { authManager } from '$lib/managers/auth-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { CancellableTask } from '$lib/utils/cancellable-task';
import type { AssetGridRouteSearchParams } from '$lib/utils/navigation';
import {
AssetMediaSize,
AssetTypeEnum,
AssetVisibility,
getAllAlbums,
getAssetInfo,
getAssetOriginalPath,
getAssetPlaybackPath,
getAssetThumbnailPath,
getBaseUrl,
type AlbumResponseDto,
type AssetResponseDto,
} from '@immich/sdk';
import { isEqual } from 'lodash-es';
export type AssetManagerOptions = {
assetId?: string;
preloadAssetIds?: string[];
size?: AssetMediaSize;
};
export class AssetManager {
isInitialized = $state(false);
#asset: AssetResponseDto | undefined = $state();
preloadAssets: TimelineAsset[] = $state([]);
cacheKey: string | null = $state(null);
albums: AlbumResponseDto[] = $state([]);
showAssetViewer: boolean = $state(false);
gridScrollTarget: AssetGridRouteSearchParams | undefined = $state();
initTask = new CancellableTask(
() => (this.isInitialized = true),
() => (this.isInitialized = false),
() => void 0,
);
// TODO: Delete this after development
#emptyAsset: AssetResponseDto = {
checksum: '',
deviceAssetId: '',
deviceId: '',
duration: '',
fileCreatedAt: '',
fileModifiedAt: '',
hasMetadata: true,
id: '',
isArchived: false,
isFavorite: false,
isOffline: false,
isTrashed: false,
localDateTime: '',
originalFileName: '',
originalPath: '',
ownerId: '',
thumbhash: null,
type: AssetTypeEnum.Image,
updatedAt: '',
visibility: AssetVisibility.Timeline,
};
static #INIT_OPTIONS = {};
#options: AssetManagerOptions = AssetManager.#INIT_OPTIONS;
constructor() {}
async #initializeAsset(id: string) {
const assetResponse = await getAssetInfo({ id, key: authManager.key });
if (!assetResponse) {
return;
}
this.#asset = assetResponse;
const albumsResponse = await getAllAlbums({ assetId: this.asset.id });
if (!albumsResponse) {
return;
}
this.albums = albumsResponse;
}
async refreshAlbums() {}
async refreshAsset() {}
async updateOptions(options: AssetManagerOptions) {
if (this.#options !== AssetManager.#INIT_OPTIONS && isEqual(this.#options, options)) {
return;
}
await this.initTask.reset();
await this.#init(options);
}
async #init(options: AssetManagerOptions) {
this.isInitialized = false;
await this.initTask.execute(async () => {
this.#options = options;
// TODO: If assetId is undefined, throw an exception.
await this.#initializeAsset(this.#options.assetId!);
// TODO: Preload assets.
}, true);
}
public destroy() {
this.isInitialized = false;
}
#createUrl(path: string, parameters?: Record<string, unknown>) {
const searchParameters = new URLSearchParams();
for (const key in parameters) {
const value = parameters[key];
if (value !== undefined && value !== null) {
searchParameters.set(key, value.toString());
}
}
return getBaseUrl() + path + searchParameters.toString();
}
get asset() {
return this.#asset ?? this.#emptyAsset;
}
get originalUrl() {
return this.#createUrl(getAssetOriginalPath(this.asset.id), { key: authManager.key, c: this.cacheKey });
}
get thumbnailUrl() {
return this.#createUrl(getAssetThumbnailPath(this.asset.id), {
key: authManager.key,
c: this.cacheKey,
size: this.#options.size,
});
}
get playbackUrl() {
return this.#createUrl(getAssetPlaybackPath(this.asset.id), { key: authManager.key, c: this.cacheKey });
}
}

View File

@@ -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();

View File

@@ -1,7 +1,6 @@
import { goto } from '$app/navigation';
import { page } from '$app/stores';
import { AppRoute } from '$lib/constants';
import { getAssetInfo } from '@immich/sdk';
import type { NavigationTarget } from '@sveltejs/kit';
import { get } from 'svelte/store';
@@ -22,10 +21,6 @@ export const isLockedFolderRoute = (route?: string | null) => !!route?.startsWit
export const isAssetViewerRoute = (target?: NavigationTarget | null) =>
!!(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() {
const $page = get(page);
// This contains special casing for the /photos/:assetId route, which hangs directly

View File

@@ -2,7 +2,7 @@ export class SlideshowHistory {
private history: { id: string }[] = [];
private index = 0;
constructor(private onChange: (asset: { id: string }) => void) {}
constructor(private onChange: (asset: { id: string }) => Promise<void>) {}
reset() {
this.history = [];
@@ -18,23 +18,23 @@ export class SlideshowHistory {
}
}
next(): boolean {
async next(): Promise<boolean> {
if (this.index === this.history.length - 1) {
return false;
}
this.index++;
this.onChange(this.history[this.index]);
await this.onChange(this.history[this.index]);
return true;
}
previous(): boolean {
async previous(): Promise<boolean> {
if (this.index === 0) {
return false;
}
this.index--;
this.onChange(this.history[this.index]);
await this.onChange(this.history[this.index]);
return true;
}
}

View File

@@ -1,32 +1,23 @@
<script lang="ts">
import { run } from 'svelte/legacy';
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';
interface Props {
children?: Snippet;
}
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.
run(() => {
if ($page.data.asset) {
setAsset($page.data.asset);
} else {
$showAssetViewer = false;
}
const asset = $page.url.searchParams.get('at');
$gridScrollTarget = { at: asset };
});
// $effect(() => {
// TODO: navigation to the asset grid.
// const asset = page.url.searchParams.get('at');
// gridScrollTarget = { at: asset };
// });
</script>
<div class:display-none={$showAssetViewer}>
<!-- display-none is based on assetManager.showAssetViewer -->
<div class:display-none={false}>
{@render children?.()}
</div>
<UploadCover />

View File

@@ -34,6 +34,7 @@
import UserAvatar from '$lib/components/shared-components/user-avatar.svelte';
import { AlbumPageViewMode, AppRoute } from '$lib/constants';
import { activityManager } from '$lib/managers/activity-manager.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
@@ -43,7 +44,6 @@
import QrCodeModal from '$lib/modals/QrCodeModal.svelte';
import SharedLinkCreateModal from '$lib/modals/SharedLinkCreateModal.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 { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
import { preferences, user } from '$lib/stores/user.store';
@@ -95,7 +95,6 @@
let { data = $bindable() }: Props = $props();
let { isViewing: showAssetViewer, setAssetId, gridScrollTarget } = assetViewingStore;
let { slideshowState, slideshowNavigation } = slideshowStore;
let oldAt: AssetGridRouteSearchParams | null | undefined = $state();
@@ -109,6 +108,15 @@
const assetInteraction = 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 }) => {
let url: string | undefined = from?.url?.pathname;
@@ -148,7 +156,8 @@
? await timelineManager.getRandomAsset()
: timelineManager.months[0]?.dayGroups[0]?.viewerAssets[0]?.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;
return;
}
if ($showAssetViewer) {
if (assetManager.showAssetViewer) {
return;
}
if (assetInteraction.selectionActive) {
@@ -346,7 +355,7 @@
const isShared = $derived(viewMode === AlbumPageViewMode.SELECT_ASSETS ? false : album.albumUsers.length > 0);
$effect(() => {
if ($showAssetViewer || !isShared) {
if (assetManager.showAssetViewer || !isShared) {
return;
}
@@ -361,7 +370,9 @@
let isOwned = $derived($user.id == album.ownerId);
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(
album.albumUsers.find(({ user: { id } }) => id === $user.id)?.role === AlbumUserRole.Editor ||
@@ -449,6 +460,7 @@
{album}
{timelineManager}
assetInteraction={currentAssetIntersection}
{assetManager}
{isShared}
{isSelectionMode}
{singleSelect}
@@ -626,7 +638,7 @@
onclick={async () => {
timelineManager.suspendTransitions = true;
viewMode = AlbumPageViewMode.SELECT_ASSETS;
oldAt = { at: $gridScrollTarget?.at };
oldAt = { at: assetManager.gridScrollTarget?.at };
await navigate(
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: { at: null } },
{ replaceState: true },
@@ -648,7 +660,7 @@
{/if}
{#if $featureFlags.loaded && $featureFlags.map}
<AlbumMap {album} />
<AlbumMap {assetManager} {album} />
{/if}
{#if album.assetCount > 0}
@@ -735,7 +747,7 @@
{/if}
{/if}
</div>
{#if album.albumUsers.length > 0 && album && isShowActivity && $user && !$showAssetViewer}
{#if album.albumUsers.length > 0 && album && isShowActivity && $user && !assetManager.showAssetViewer}
<div class="flex">
<div
transition:fly={{ duration: 150 }}

View File

@@ -1,18 +1,14 @@
import { authenticate } from '$lib/utils/auth';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getAlbumInfo } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const [album, asset] = await Promise.all([
getAlbumInfo({ id: params.albumId, withoutAssets: true }),
getAssetInfoFromParam(params),
]);
const album = await getAlbumInfo({ id: params.albumId, withoutAssets: true });
return {
album,
asset,
assetId: params.assetId,
meta: {
title: album.albumName,
},

View File

@@ -14,6 +14,7 @@
import { AssetAction } from '$lib/constants';
import SetVisibilityAction from '$lib/components/photos-page/actions/set-visibility-action.svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk';
@@ -33,6 +34,15 @@
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 = () => {
if (assetInteraction.selectionActive) {
assetInteraction.clearMultiselect();
@@ -51,6 +61,7 @@
enableRouting={true}
{timelineManager}
{assetInteraction}
{assetManager}
removeAction={AssetAction.UNARCHIVE}
onEscape={handleEscape}
>

View File

@@ -1,15 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
meta: {
title: $t('archive'),
},

View File

@@ -17,6 +17,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { preferences } from '$lib/stores/user.store';
@@ -37,6 +38,15 @@
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 = () => {
if (assetInteraction.selectionActive) {
assetInteraction.clearMultiselect();
@@ -56,6 +66,7 @@
withStacked={true}
{timelineManager}
{assetInteraction}
{assetManager}
removeAction={AssetAction.UNFAVORITE}
onEscape={handleEscape}
>

View File

@@ -1,15 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
meta: {
title: $t('favorites'),
},

View File

@@ -21,8 +21,8 @@
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
import Sidebar from '$lib/components/sidebar/sidebar.svelte';
import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
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 { preferences } from '$lib/stores/user.store';
import { cancelMultiselect } from '$lib/utils/asset-utils';
@@ -32,6 +32,8 @@
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { onDestroy } from 'svelte';
interface Props {
data: PageData;
@@ -43,6 +45,15 @@
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));
function getLinkForPath(path: string) {
@@ -106,6 +117,7 @@
<GalleryViewer
assets={data.pathAssets}
{assetInteraction}
{assetManager}
{viewport}
showAssetName={true}
pageHeaderOffset={54}

View File

@@ -2,12 +2,11 @@ import { QueryParameter } from '$lib/constants';
import { foldersStore } from '$lib/stores/folders.svelte';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, 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!;
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;
return {
asset,
assetId: params.assetId,
tree,
pathAssets,
meta: {

View File

@@ -12,6 +12,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AppRoute, AssetAction } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility, lockAuthSession } from '@immich/sdk';
@@ -33,6 +34,15 @@
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 = () => {
if (assetInteraction.selectionActive) {
assetInteraction.clearMultiselect();
@@ -62,6 +72,7 @@
enableRouting={true}
{timelineManager}
{assetInteraction}
{assetManager}
onEscape={handleEscape}
removeAction={AssetAction.SET_VISIBILITY_TIMELINE}
>

View File

@@ -1,7 +1,6 @@
import { AppRoute } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getAuthStatus } from '@immich/sdk';
import { redirect } from '@sveltejs/kit';
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)}`);
}
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
meta: {
title: $t('locked_folder'),
},

View File

@@ -1,12 +1,10 @@
<script lang="ts">
import { run } from 'svelte/legacy';
import { goto } from '$app/navigation';
import UserPageLayout from '$lib/components/layouts/user-page-layout.svelte';
import Map from '$lib/components/shared-components/map/map.svelte';
import Portal from '$lib/components/shared-components/portal/portal.svelte';
import { AppRoute } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { featureFlags } from '$lib/stores/server-config.store';
import { handlePromiseError } from '$lib/utils';
import { navigate } from '$lib/utils/navigation';
@@ -19,16 +17,23 @@
let { data }: Props = $props();
let { isViewing: showAssetViewer, asset: viewingAsset, setAssetId } = assetViewingStore;
let viewingAssets: string[] = $state([]);
let viewingAssetCursor = 0;
const assetManager = new AssetManager();
$effect(() => {
if (data.assetId) {
assetManager.showAssetViewer = true;
void assetManager.updateOptions({ assetId: data.assetId });
}
});
onDestroy(() => assetManager.destroy());
onDestroy(() => {
assetViewingStore.showAssetViewer(false);
assetManager.showAssetViewer = false;
});
run(() => {
$effect(() => {
if (!$featureFlags.map) {
handlePromiseError(goto(AppRoute.PHOTOS));
}
@@ -37,13 +42,12 @@
async function onViewAssets(assetIds: string[]) {
viewingAssets = assetIds;
viewingAssetCursor = 0;
await setAssetId(assetIds[0]);
await navigate({ targetRoute: 'current', assetId: assetIds[0] });
}
async function navigateNext() {
if (viewingAssetCursor < viewingAssets.length - 1) {
await setAssetId(viewingAssets[++viewingAssetCursor]);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
await navigate({ targetRoute: 'current', assetId: viewingAssets[++viewingAssetCursor] });
return true;
}
return false;
@@ -51,21 +55,19 @@
async function navigatePrevious() {
if (viewingAssetCursor > 0) {
await setAssetId(viewingAssets[--viewingAssetCursor]);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
await navigate({ targetRoute: 'current', assetId: viewingAssets[--viewingAssetCursor] });
return true;
}
return false;
}
async function navigateRandom() {
if (viewingAssets.length <= 0) {
return undefined;
if (viewingAssets.length > 0) {
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);
const asset = await setAssetId(viewingAssets[index]);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
return asset;
return false;
}
</script>
@@ -76,16 +78,16 @@
</div>
</UserPageLayout>
<Portal target="body">
{#if $showAssetViewer}
{#if assetManager.showAssetViewer}
{#await import('../../../../../lib/components/asset-viewer/asset-viewer.svelte') then { default: AssetViewer }}
<AssetViewer
asset={$viewingAsset}
{assetManager}
showNavigation={viewingAssets.length > 1}
onNext={navigateNext}
onPrevious={navigatePrevious}
onRandom={navigateRandom}
onClose={() => {
assetViewingStore.showAssetViewer(false);
assetManager.showAssetViewer = false;
handlePromiseError(navigate({ targetRoute: 'current', assetId: null }));
}}
isShared={false}

View File

@@ -1,15 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
meta: {
title: $t('map'),
},

View File

@@ -1,5 +1,23 @@
<script>
<script lang="ts">
import MemoryViewer from '$lib/components/memory-page/memory-viewer.svelte';
import { AssetManager } from '$lib/managers/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>
<MemoryViewer />
<MemoryViewer {assetManager} />

View File

@@ -1,16 +1,14 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
const user = await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
user,
asset,
assetId: params.assetId,
meta: {
title: $t('memory'),
},

View File

@@ -8,6 +8,7 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
import { AppRoute } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { AssetVisibility } from '@immich/sdk';
@@ -34,6 +35,15 @@
onDestroy(() => timelineManager.destroy());
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 = () => {
if (assetInteraction.selectionActive) {
assetInteraction.clearMultiselect();
@@ -43,7 +53,7 @@
</script>
<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>
{#if assetInteraction.selectionActive}

View File

@@ -1,6 +1,5 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getUser } from '@immich/sdk';
import type { PageLoad } from './$types';
@@ -8,11 +7,10 @@ export const load = (async ({ params, url }) => {
await authenticate(url);
const partner = await getUser({ id: params.userId });
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
partner,
meta: {
title: $t('partner'),

View File

@@ -31,13 +31,13 @@
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import PersonEditBirthDateModal from '$lib/modals/PersonEditBirthDateModal.svelte';
import PersonMergeSuggestionModal from '$lib/modals/PersonMergeSuggestionModal.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 { preferences } from '$lib/stores/user.store';
import { websocketEvents } from '$lib/stores/websocket';
@@ -75,7 +75,6 @@
let { data }: Props = $props();
let numberOfAssets = $state(data.statistics.assets);
let { isViewing: showAssetViewer } = assetViewingStore;
const timelineManager = new TimelineManager();
$effect(() => void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, personId: data.person.id }));
@@ -83,6 +82,15 @@
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 isEditingName = $state(false);
let previousRoute: string = $state(AppRoute.EXPLORE);
@@ -123,7 +131,7 @@
});
const handleEscape = async () => {
if ($showAssetViewer) {
if (assetManager.showAssetViewer) {
return;
}
if (assetInteraction.selectionActive) {
@@ -388,6 +396,7 @@
{person}
{timelineManager}
{assetInteraction}
{assetManager}
isSelectionMode={viewMode === PersonPageViewMode.SELECT_PERSON}
singleSelect={viewMode === PersonPageViewMode.SELECT_PERSON}
onSelect={handleSelectFeaturePhoto}

View File

@@ -1,23 +1,21 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getPerson, getPersonStatistics } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const [person, statistics, asset] = await Promise.all([
const [person, statistics] = await Promise.all([
getPerson({ id: params.personId }),
getPersonStatistics({ id: params.personId }),
getAssetInfoFromParam(params),
]);
const $t = await getFormatter();
return {
person,
statistics,
asset,
assetId: params.assetId,
meta: {
title: person.name || $t('person'),
},

View File

@@ -22,9 +22,9 @@
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
import { AssetAction } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.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 { preferences, user } from '$lib/stores/user.store';
import {
@@ -39,12 +39,27 @@
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
import { onDestroy } from 'svelte';
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();
void timelineManager.updateOptions({ visibility: AssetVisibility.Timeline, withStacked: true, withPartners: true });
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();
let selectedAssets = $derived(assetInteraction.selectedAssets);
@@ -59,7 +74,7 @@
return assetInteraction.isAllUserOwned && (isLivePhoto || isLivePhotoCandidate);
});
const handleEscape = () => {
if ($showAssetViewer) {
if (assetManager.showAssetViewer) {
return;
}
if (assetInteraction.selectionActive) {
@@ -93,6 +108,7 @@
enableRouting={true}
{timelineManager}
{assetInteraction}
{assetManager}
removeAction={AssetAction.ARCHIVE}
onEscape={handleEscape}
withStacked

View File

@@ -1,15 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
meta: {
title: $t('photos'),
},

View File

@@ -23,10 +23,10 @@
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset, Viewport } from '$lib/managers/timeline-manager/types';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { lang, locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { preferences } from '$lib/stores/user.store';
@@ -47,11 +47,17 @@
} from '@immich/sdk';
import { IconButton } from '@immich/ui';
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import { tick } from 'svelte';
import { onDestroy, tick } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
interface Props {
data: PageData;
}
let { data }: Props = $props();
const MAX_ASSET_COUNT = 5000;
let { isViewing: showAssetViewer } = assetViewingStore;
const viewport: Viewport = $state({ width: 0, height: 0 });
// The GalleryViewer pushes it's own history state, which causes weird
@@ -83,8 +89,17 @@
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 = () => {
if ($showAssetViewer) {
if (assetManager.showAssetViewer) {
return;
}
@@ -379,6 +394,7 @@
<GalleryViewer
assets={searchResultAssets}
{assetInteraction}
{assetManager}
onIntersected={loadNextPage}
showArchiveIcon={true}
{viewport}

View File

@@ -1,15 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
meta: {
title: $t('search'),
},

View File

@@ -5,14 +5,14 @@
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
import PasswordField from '$lib/components/shared-components/password-field.svelte';
import ThemeButton from '$lib/components/shared-components/theme-button.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { user } from '$lib/stores/user.store';
import { setSharedLink } from '$lib/utils';
import { handleError } from '$lib/utils/handle-error';
import { navigate } from '$lib/utils/navigation';
import { getMySharedLink, SharedLinkType } from '@immich/sdk';
import { Button } from '@immich/ui';
import { tick } from 'svelte';
import { onDestroy, tick } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
@@ -22,12 +22,20 @@
let { data }: Props = $props();
let { gridScrollTarget } = assetViewingStore;
let { sharedLink, passwordRequired, sharedLinkKey: key, meta } = $state(data);
let { title, description } = $state(meta);
let isOwned = $derived($user ? $user.id === sharedLink?.userId : false);
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 () => {
try {
sharedLink = await getMySharedLink({ password, key });
@@ -39,7 +47,7 @@
$t('shared_photos_and_videos_count', { values: { assetCount: sharedLink.assets.length } });
await tick();
await navigate(
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: $gridScrollTarget },
{ targetRoute: 'current', assetId: null, assetGridRouteSearchParams: assetManager.gridScrollTarget },
{ forceNavigate: true, replaceState: true },
);
} catch (error) {
@@ -88,10 +96,10 @@
{/if}
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Album}
<AlbumViewer {sharedLink} />
<AlbumViewer {assetManager} {sharedLink} />
{/if}
{#if !passwordRequired && sharedLink?.type == SharedLinkType.Individual}
<div class="immich-scrollbar">
<IndividualSharedViewer {sharedLink} {isOwned} />
<IndividualSharedViewer {assetManager} {sharedLink} {isOwned} />
</div>
{/if}

View File

@@ -1,7 +1,6 @@
import { getAssetThumbnailUrl, setSharedLink } from '$lib/utils';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getMySharedLink, isHttpError } from '@immich/sdk';
import type { PageLoad } from './$types';
@@ -12,7 +11,7 @@ export const load = (async ({ params, url }) => {
const $t = await getFormatter();
try {
const [sharedLink, asset] = await Promise.all([getMySharedLink({ key }), getAssetInfoFromParam(params)]);
const sharedLink = await getMySharedLink({ key });
setSharedLink(sharedLink);
const assetCount = sharedLink.assets.length;
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
@@ -21,7 +20,7 @@ export const load = (async ({ params, url }) => {
return {
sharedLink,
sharedLinkKey: key,
asset,
assetId: params.assetId,
meta: {
title: sharedLink.album ? sharedLink.album.albumName : $t('public_share'),
description: sharedLink.description || $t('shared_photos_and_videos_count', { values: { assetCount } }),

View File

@@ -20,6 +20,7 @@
import { onDestroy } from 'svelte';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
interface Props {
data: PageData;
@@ -33,6 +34,15 @@
$effect(() => void timelineManager.updateOptions({ deferInit: !tag, tagId: tag?.id }));
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);
const tree = $derived(TreeNode.fromTags(tags));
const tag = $derived(tree.traverse(data.path));
@@ -118,7 +128,13 @@
<section class="mt-2 h-[calc(100%-(--spacing(20)))] overflow-auto immich-scrollbar">
{#if tag.hasAssets}
<AssetGrid enableRouting={true} {timelineManager} {assetInteraction} removeAction={AssetAction.UNARCHIVE}>
<AssetGrid
enableRouting={true}
{timelineManager}
{assetInteraction}
{assetManager}
removeAction={AssetAction.UNARCHIVE}
>
{#snippet empty()}
<TreeItemThumbnails items={tag.children} icon={mdiTag} onClick={handleNavigation} />
{/snippet}

View File

@@ -1,13 +1,11 @@
import { QueryParameter } from '$lib/constants';
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getAllTags } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
const tags = await getAllTags();
@@ -15,7 +13,7 @@ export const load = (async ({ params, url }) => {
return {
path: url.searchParams.get(QueryParameter.PATH) ?? '',
tags,
asset,
assetId: params.assetId,
meta: {
title: $t('tags'),
},

View File

@@ -13,6 +13,7 @@
notificationController,
} from '$lib/components/shared-components/notification/notification';
import { AppRoute } from '$lib/constants';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
import { modalManager } from '$lib/managers/modal-manager.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
@@ -42,6 +43,15 @@
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 isConfirmed = await modalManager.showDialog({ prompt: $t('empty_trash_confirmation') });
if (!isConfirmed) {
@@ -117,7 +127,7 @@
</HStack>
{/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">
{$t('trashed_items_will_be_permanently_deleted_after', { values: { days: $serverConfig.trashDays } })}
</p>

View File

@@ -1,15 +1,13 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
meta: {
title: $t('trash'),
},

View File

@@ -1,15 +1,12 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
export const load = (async ({ url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const $t = await getFormatter();
return {
asset,
meta: {
title: $t('utilities'),
},

View File

@@ -19,6 +19,8 @@
import { mdiCheckOutline, mdiInformationOutline, mdiKeyboard, mdiTrashCanOutline } from '@mdi/js';
import { t } from 'svelte-i18n';
import type { PageData } from './$types';
import { onDestroy } from 'svelte';
import { AssetManager } from '$lib/managers/asset-manager.svelte';
interface Props {
data: PageData;
@@ -36,6 +38,15 @@
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 = {
general: [],
actions: [
@@ -207,6 +218,7 @@
{#key duplicates[0].duplicateId}
<DuplicatesCompareControl
assets={duplicates[0].assets}
{assetManager}
onResolve={(duplicateAssetIds, trashIds) =>
handleResolve(duplicates[0].duplicateId, duplicateAssetIds, trashIds)}
onStack={(assets) => handleStack(duplicates[0].duplicateId, assets)}

View File

@@ -1,17 +1,15 @@
import { authenticate } from '$lib/utils/auth';
import { getFormatter } from '$lib/utils/i18n';
import { getAssetInfoFromParam } from '$lib/utils/navigation';
import { getAssetDuplicates } from '@immich/sdk';
import type { PageLoad } from './$types';
export const load = (async ({ params, url }) => {
await authenticate(url);
const asset = await getAssetInfoFromParam(params);
const duplicates = await getAssetDuplicates();
const $t = await getFormatter();
return {
asset,
assetId: params.assetId,
duplicates,
meta: {
title: $t('duplicates'),