GalleryViewer

This commit is contained in:
Min Idzelis 2025-04-20 02:51:32 +00:00
parent 3b9490e28d
commit c1e699ebaf
12 changed files with 168 additions and 154 deletions

View File

@ -66,7 +66,7 @@
onClose: (asset: AssetResponseDto) => void;
onNext: () => Promise<HasAsset>;
onPrevious: () => Promise<HasAsset>;
onRandom: () => Promise<AssetResponseDto | undefined>;
onRandom: () => Promise<{ id: string } | undefined>;
copyImage?: () => Promise<void>;
}
@ -89,7 +89,7 @@
copyImage = $bindable(),
}: Props = $props();
const { setAsset } = assetViewingStore;
const { setAssetId } = assetViewingStore;
const {
restartProgress: restartSlideshowProgress,
stopProgress: stopSlideshowProgress,
@ -210,7 +210,7 @@
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
if (value === SlideshowState.PlaySlideshow) {
slideshowHistory.reset();
slideshowHistory.queue(asset);
slideshowHistory.queue(toTimelineAsset(asset));
handlePromiseError(handlePlaySlideshow());
} else if (value === SlideshowState.StopSlideshow) {
handlePromiseError(handleStopSlideshow());
@ -220,7 +220,7 @@
shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => {
if (value === SlideshowNavigation.Shuffle) {
slideshowHistory.reset();
slideshowHistory.queue(asset);
slideshowHistory.queue(toTimelineAsset(asset));
}
});
@ -335,8 +335,7 @@
let assetViewerHtmlElement = $state<HTMLElement>();
const slideshowHistory = new SlideshowHistory((asset) => {
setAsset(asset);
$restartSlideshowProgress = true;
handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true)));
});
const handleVideoStarted = () => {

View File

@ -26,14 +26,15 @@
import { AppRoute, QueryParameter } from '$lib/constants';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { type Viewport } from '$lib/stores/assets-store.svelte';
import { type TimelineAsset, type Viewport } from '$lib/stores/assets-store.svelte';
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';
import { getAssetPlaybackUrl, getAssetThumbnailUrl, handlePromiseError, memoryLaneTitle } from '$lib/utils';
import { getAssetPlaybackUrl, getAssetThumbnailUrl, getKey, handlePromiseError, memoryLaneTitle } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import { fromLocalDateTime } from '$lib/utils/timeline-util';
import { AssetMediaSize, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
import { getAltText } from '$lib/utils/thumbnail-util';
import { fromLocalDateTime, toTimelineAsset } from '$lib/utils/timeline-util';
import { AssetMediaSize, getAssetInfo } from '@immich/sdk';
import { IconButton } from '@immich/ui';
import {
mdiCardsOutline,
@ -66,6 +67,11 @@
let playerInitialized = $state(false);
let paused = $state(false);
let current = $state<MemoryAsset | undefined>(undefined);
let currentMemoryAssetFull = $derived.by(async () =>
current?.asset ? await getAssetInfo({ id: current?.asset.id, key: getKey() }) : undefined,
);
let currentTimelineAssets = $derived(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
let isSaved = $derived(current?.memory.isSaved);
let viewerHeight = $state(0);
@ -73,11 +79,11 @@
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 });
const assetInteraction = new AssetInteraction<AssetResponseDto>();
const assetInteraction = new AssetInteraction();
let progressBarController: Tween<number> | undefined = $state(undefined);
let videoPlayer: HTMLVideoElement | undefined = $state();
const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`;
const handleNavigate = async (asset?: AssetResponseDto) => {
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
const handleNavigate = async (asset?: { id: string }) => {
if ($isViewing) {
return asset;
}
@ -88,9 +94,9 @@
await goto(asHref(asset));
};
const setProgressDuration = (asset: AssetResponseDto) => {
if (asset.type === AssetTypeEnum.Video) {
const timeParts = asset.duration.split(':').map(Number);
const setProgressDuration = (asset: TimelineAsset) => {
if (asset.isVideo) {
const timeParts = asset.duration!.split(':').map(Number);
const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000;
progressBarController = new Tween<number>(0, {
duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0),
@ -106,7 +112,8 @@
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
const handleEscape = async () => goto(AppRoute.PHOTOS);
const handleSelectAll = () => assetInteraction.selectAssets(current?.memory.assets || []);
const handleSelectAll = () =>
assetInteraction.selectAssets(current?.memory.assets.map((a) => toTimelineAsset(a)) || []);
const handleAction = async (callingContext: string, action: 'reset' | 'pause' | 'play') => {
// leaving these log statements here as comments. Very useful to figure out what's going on during dev!
// console.log(`handleAction[${callingContext}] called with: ${action}`);
@ -239,7 +246,7 @@
};
const initPlayer = () => {
const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.type === AssetTypeEnum.Video && !videoPlayer;
const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.isVideo && !videoPlayer;
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
return;
}
@ -439,7 +446,7 @@
<div class="relative h-full w-full rounded-2xl bg-black">
{#key current.asset.id}
<div transition:fade class="h-full w-full">
{#if current.asset.type === AssetTypeEnum.Video}
{#if current.asset.isVideo}
<video
bind:this={videoPlayer}
autoplay
@ -453,13 +460,15 @@
transition:fade
></video>
{:else}
<img
class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
alt={current.asset.exifInfo?.description}
draggable="false"
transition:fade
/>
{#await currentMemoryAssetFull then asset}
<img
class="h-full w-full rounded-2xl object-contain transition-all"
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
alt={$getAltText(asset)}
draggable="false"
transition:fade
/>
{/await}
{/if}
</div>
{/key}
@ -547,8 +556,10 @@
{fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
</p>
<p>
{current.asset.exifInfo?.city || ''}
{current.asset.exifInfo?.country || ''}
{#await currentMemoryAssetFull then asset}
{asset?.exifInfo?.city || ''}
{asset?.exifInfo?.country || ''}
{/await}
</p>
</div>
</div>
@ -619,7 +630,7 @@
<GalleryViewer
onNext={handleNextAsset}
onPrevious={handlePreviousAsset}
assets={current.memory.assets}
assets={currentTimelineAssets}
viewport={galleryViewport}
{assetInteraction}
slidingWindowOffset={viewerHeight}

View File

@ -1,27 +1,27 @@
<script lang="ts">
import { goto } from '$app/navigation';
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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { getKey, handlePromiseError } from '$lib/utils';
import { downloadArchive } from '$lib/utils/asset-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 { addSharedLinkAssets, type AssetResponseDto, type SharedLinkResponseDto } from '@immich/sdk';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { addSharedLinkAssets, getAssetInfo, type SharedLinkResponseDto } from '@immich/sdk';
import { mdiArrowLeft, mdiFileImagePlusOutline, mdiFolderDownloadOutline, mdiSelectAll } from '@mdi/js';
import { t } from 'svelte-i18n';
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import DownloadAction from '../photos-page/actions/download-action.svelte';
import RemoveFromSharedLink from '../photos-page/actions/remove-from-shared-link.svelte';
import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte';
import ControlAppBar from '../shared-components/control-app-bar.svelte';
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
import AssetViewer from '../asset-viewer/asset-viewer.svelte';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { t } from 'svelte-i18n';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
interface Props {
sharedLink: SharedLinkResponseDto;
@ -31,9 +31,10 @@
let { sharedLink = $bindable(), isOwned }: Props = $props();
const viewport: Viewport = $state({ width: 0, height: 0 });
const assetInteraction = new AssetInteraction<AssetResponseDto>();
const assetInteraction = new AssetInteraction();
let assets = $derived(sharedLink.assets);
let assets = $derived(sharedLink.assets.map((a) => toTimelineAsset(a)));
let fullAsset = $derived(assets[0] ? getAssetInfo({ id: assets[0]?.id, key: getKey() }) : null);
dragAndDropFilesStore.subscribe((value) => {
if (value.isDragging && value.files.length > 0) {
@ -127,14 +128,16 @@
<GalleryViewer {assets} {assetInteraction} {viewport} />
</section>
{:else}
<AssetViewer
asset={assets[0]}
showCloseButton={false}
onAction={handleAction}
onPrevious={() => Promise.resolve(false)}
onNext={() => Promise.resolve(false)}
onRandom={() => Promise.resolve(undefined)}
onClose={() => {}}
/>
{#await fullAsset then asset}
<AssetViewer
asset={asset!}
showCloseButton={false}
onAction={handleAction}
onPrevious={() => Promise.resolve(false)}
onNext={() => Promise.resolve(false)}
onRandom={() => Promise.resolve(undefined)}
onClose={() => {}}
/>
{/await}
{/if}
</section>

View File

@ -6,7 +6,7 @@
import { AppRoute, AssetAction } from '$lib/constants';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { handlePromiseError } from '$lib/utils';
@ -25,17 +25,17 @@
import ShowShortcuts from '../show-shortcuts.svelte';
interface Props {
assets: AssetResponseDto[];
assetInteraction: AssetInteraction<AssetResponseDto>;
assets: (TimelineAsset | AssetResponseDto)[];
assetInteraction: AssetInteraction;
disableAssetSelect?: boolean;
showArchiveIcon?: boolean;
viewport: Viewport;
onIntersected?: (() => void) | undefined;
showAssetName?: boolean;
isShowDeleteConfirmation?: boolean;
onPrevious?: (() => Promise<AssetResponseDto | undefined>) | undefined;
onNext?: (() => Promise<AssetResponseDto | undefined>) | undefined;
onRandom?: (() => Promise<AssetResponseDto | undefined>) | undefined;
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
pageHeaderOffset?: number;
slidingWindowOffset?: number;
}
@ -56,7 +56,7 @@
pageHeaderOffset = 0,
}: Props = $props();
let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore;
let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
let geometry: CommonJustifiedLayout | undefined = $state();
@ -83,19 +83,26 @@
containerHeight = geometry.containerHeight;
containerWidth = geometry.containerWidth;
for (const [i, asset] of assets.entries()) {
const layout = {
asset,
top: geometry.getTop(i),
left: geometry.getLeft(i),
width: geometry.getWidth(i),
height: geometry.getHeight(i),
};
// 54 is the content height of the asset-selection-app-bar
const layoutTopWithOffset = layout.top + pageHeaderOffset;
const layoutBottom = layoutTopWithOffset + layout.height;
const top = geometry.getTop(i);
const left = geometry.getLeft(i);
const width = geometry.getWidth(i);
const height = geometry.getHeight(i);
const layoutTopWithOffset = top + pageHeaderOffset;
const layoutBottom = layoutTopWithOffset + height;
const display = layoutTopWithOffset < slidingWindow.bottom && layoutBottom > slidingWindow.top;
assetLayout.push({ ...layout, display });
const layout = {
asset,
top,
left,
width,
height,
display,
};
assetLayout.push(layout);
}
}
@ -109,7 +116,7 @@
let showShortcuts = $state(false);
let currentViewAssetIndex = 0;
let shiftKeyIsDown = $state(false);
let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
let slidingWindow = $state({ top: 0, bottom: 0 });
const updateSlidingWindow = () => {
@ -139,14 +146,14 @@
}
}
});
const viewAssetHandler = async (asset: AssetResponseDto) => {
const viewAssetHandler = async (asset: TimelineAsset) => {
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
setAsset(assets[currentViewAssetIndex]);
await setAssetId(assets[currentViewAssetIndex].id);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
};
const selectAllAssets = () => {
assetInteraction.selectAssets(assets);
assetInteraction.selectAssets(assets.map((a) => toTimelineAsset(a)));
};
const deselectAllAssets = () => {
@ -168,7 +175,7 @@
}
};
const handleSelectAssets = (asset: AssetResponseDto) => {
const handleSelectAssets = (asset: TimelineAsset) => {
if (!asset) {
return;
}
@ -191,14 +198,14 @@
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
};
const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
if (asset) {
selectAssetCandidates(asset);
}
lastAssetMouseEvent = asset;
};
const selectAssetCandidates = (endAsset: AssetResponseDto) => {
const selectAssetCandidates = (endAsset: TimelineAsset) => {
if (!shiftKeyIsDown) {
return;
}
@ -215,7 +222,7 @@
[start, end] = [end, start];
}
assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1));
assetInteraction.setAssetSelectionCandidates(assets.slice(start, end + 1).map((a) => toTimelineAsset(a)));
};
const onSelectStart = (e: Event) => {
@ -310,7 +317,7 @@
const handleNext = async (): Promise<boolean> => {
try {
let asset: AssetResponseDto | undefined;
let asset: { id: string } | undefined;
if (onNext) {
asset = await onNext();
} else {
@ -334,9 +341,9 @@
}
};
const handleRandom = async (): Promise<AssetResponseDto | undefined> => {
const handleRandom = async (): Promise<{ id: string } | undefined> => {
try {
let asset: AssetResponseDto | undefined;
let asset: { id: string } | undefined;
if (onRandom) {
asset = await onRandom();
} else {
@ -360,7 +367,7 @@
const handlePrevious = async (): Promise<boolean> => {
try {
let asset: AssetResponseDto | undefined;
let asset: { id: string } | undefined;
if (onPrevious) {
asset = await onPrevious();
} else {
@ -384,9 +391,9 @@
}
};
const navigateToAsset = async (asset?: AssetResponseDto) => {
const navigateToAsset = async (asset?: { id: string }) => {
if (asset && asset.id !== $viewingAsset.id) {
setAsset(asset);
await setAssetId(asset.id);
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
}
};
@ -405,20 +412,20 @@
} else if (currentViewAssetIndex === assets.length) {
await handlePrevious();
} else {
setAsset(assets[currentViewAssetIndex]);
await setAssetId(assets[currentViewAssetIndex].id);
}
break;
}
}
};
const assetMouseEventHandler = (asset: AssetResponseDto | null) => {
const assetMouseEventHandler = (asset: TimelineAsset | null) => {
if (assetInteraction.selectionActive) {
handleSelectAssetCandidates(asset);
}
};
const assetOnFocusHandler = (asset: AssetResponseDto) => {
const assetOnFocusHandler = (asset: TimelineAsset) => {
assetInteraction.focussedAssetId = asset.id;
};
@ -478,20 +485,19 @@
class="absolute"
style:overflow="clip"
style="width: {layout.width}px; height: {layout.height}px; top: {layout.top}px; left: {layout.left}px"
title={showAssetName ? asset.originalFileName : ''}
>
<Thumbnail
readonly={disableAssetSelect}
onClick={() => {
if (assetInteraction.selectionActive) {
handleSelectAssets(asset);
handleSelectAssets(toTimelineAsset(asset));
return;
}
void viewAssetHandler(asset);
void viewAssetHandler(toTimelineAsset(asset));
}}
onSelect={() => handleSelectAssets(asset)}
onMouseEvent={() => assetMouseEventHandler(asset)}
handleFocus={() => assetOnFocusHandler(asset)}
onSelect={() => handleSelectAssets(toTimelineAsset(asset))}
onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(asset))}
handleFocus={() => assetOnFocusHandler(toTimelineAsset(asset))}
{showArchiveIcon}
asset={toTimelineAsset(asset)}
selected={assetInteraction.hasSelectedAsset(asset.id)}
@ -500,11 +506,13 @@
thumbnailWidth={layout.width}
thumbnailHeight={layout.height}
/>
<!-- note: if using showAssetName then the 'assets' prop must be AssetResponseDto (only used by folders) -->
{#if showAssetName}
<div
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-gradient-to-t bg-slate-50/75 overflow-clip text-ellipsis whitespace-pre-wrap"
>
{asset.originalFileName}
{@debug}
{(asset as AssetResponseDto).originalFileName}
</div>
{/if}
</div>

View File

@ -1,27 +1,20 @@
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { user } from '$lib/stores/user.store';
import type { AssetStackResponseDto, UserAdminResponseDto } from '@immich/sdk';
import type { UserAdminResponseDto } from '@immich/sdk';
import { SvelteSet } from 'svelte/reactivity';
import { fromStore } from 'svelte/store';
export type BaseInteractionAsset = {
id: string;
isTrashed: boolean;
isArchived: boolean;
isFavorite: boolean;
ownerId: string;
stack?: AssetStackResponseDto | null | undefined;
};
export class AssetInteraction<T extends BaseInteractionAsset> {
selectedAssets = $state<T[]>([]);
export class AssetInteraction {
selectedAssets = $state<TimelineAsset[]>([]);
hasSelectedAsset(assetId: string) {
return this.selectedAssets.some((asset) => asset.id === assetId);
}
selectedGroup = new SvelteSet<string>();
assetSelectionCandidates = $state<T[]>([]);
assetSelectionCandidates = $state<TimelineAsset[]>([]);
hasSelectionCandidate(assetId: string) {
return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
}
assetSelectionStart = $state<T | null>(null);
assetSelectionStart = $state<TimelineAsset | null>(null);
focussedAssetId = $state<string | null>(null);
selectionActive = $derived(this.selectedAssets.length > 0);
@ -33,13 +26,13 @@ export class AssetInteraction<T extends BaseInteractionAsset> {
isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
selectAsset(asset: T) {
selectAsset(asset: TimelineAsset) {
if (!this.hasSelectedAsset(asset.id)) {
this.selectedAssets.push(asset);
}
}
selectAssets(assets: T[]) {
selectAssets(assets: TimelineAsset[]) {
for (const asset of assets) {
this.selectAsset(asset);
}
@ -60,11 +53,11 @@ export class AssetInteraction<T extends BaseInteractionAsset> {
this.selectedGroup.delete(group);
}
setAssetSelectionStart(asset: T | null) {
setAssetSelectionStart(asset: TimelineAsset | null) {
this.assetSelectionStart = asset;
}
setAssetSelectionCandidates(assets: T[]) {
setAssetSelectionCandidates(assets: TimelineAsset[]) {
this.assetSelectionCandidates = assets;
}

View File

@ -1,12 +1,7 @@
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
import { asLocalTimeISO } from '$lib/utils/date-time';
import {
type AssetResponseDto,
deleteMemory,
type MemoryResponseDto,
removeMemoryAssets,
searchMemories,
updateMemory,
} from '@immich/sdk';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
import { DateTime } from 'luxon';
type MemoryIndex = {
@ -16,7 +11,7 @@ type MemoryIndex = {
export type MemoryAsset = MemoryIndex & {
memory: MemoryResponseDto;
asset: AssetResponseDto;
asset: TimelineAsset;
previousMemory?: MemoryResponseDto;
previous?: MemoryAsset;
next?: MemoryAsset;
@ -36,7 +31,7 @@ class MemoryStoreSvelte {
memoryIndex,
previousMemory: this.memories[memoryIndex - 1],
nextMemory: this.memories[memoryIndex + 1],
asset,
asset: toTimelineAsset(asset),
assetIndex,
previous,
};

View File

@ -1,17 +1,15 @@
import type { AssetResponseDto } from '@immich/sdk';
export class SlideshowHistory {
private history: AssetResponseDto[] = [];
private history: { id: string }[] = [];
private index = 0;
constructor(private onChange: (asset: AssetResponseDto) => void) {}
constructor(private onChange: (asset: { id: string }) => void) {}
reset() {
this.history = [];
this.index = 0;
}
queue(asset: AssetResponseDto) {
queue(asset: { id: string }) {
this.history.push(asset);
// If we were at the end of the slideshow history, move the index to the new end

View File

@ -37,8 +37,16 @@ export function getThumbnailSize(assetCount: number, viewWidth: number): number
return 300;
}
export const getAltTextForTimelineAsset = () => {
// TODO: implement this in a performant way
return '';
};
export const getAltText = derived(t, ($t) => {
return (asset: AssetResponseDto) => {
return (asset: AssetResponseDto | null | undefined) => {
if (!asset) {
return '';
}
if (asset.exifInfo?.description) {
return asset.exifInfo.description;
}

View File

@ -1,4 +1,3 @@
import type { BaseInteractionAsset } from '$lib/stores/asset-interaction.svelte';
import type { AssetBucket, TimelineAsset } from '$lib/stores/assets-store.svelte';
import { locale } from '$lib/stores/preferences.store';
import { getAssetRatio } from '$lib/utils/asset-utils';
@ -108,7 +107,7 @@ export const getDateLocaleString = (date: DateTime, opts?: LocaleOptions): strin
export const formatDateGroupTitle = memoize(formatGroupTitle);
export const toTimelineAsset = (unknownAsset: BaseInteractionAsset): TimelineAsset => {
export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): TimelineAsset => {
if (isTimelineAsset(unknownAsset)) {
return unknownAsset;
}
@ -132,5 +131,5 @@ export const toTimelineAsset = (unknownAsset: BaseInteractionAsset): TimelineAss
livePhotoVideoId: assetResponse.livePhotoVideoId || null,
};
};
export const isTimelineAsset = (arg: BaseInteractionAsset): arg is TimelineAsset =>
export const isTimelineAsset = (arg: AssetResponseDto | TimelineAsset): arg is TimelineAsset =>
(arg as TimelineAsset).ratio !== undefined;

View File

@ -27,8 +27,8 @@
import { foldersStore } from '$lib/stores/folders.svelte';
import { preferences } from '$lib/stores/user.store';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
import type { AssetResponseDto } from '@immich/sdk';
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import { onMount } from 'svelte';
import { t } from 'svelte-i18n';
@ -47,7 +47,7 @@
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
const assetInteraction = new AssetInteraction<AssetResponseDto>();
const assetInteraction = new AssetInteraction();
onMount(async () => {
await foldersStore.fetchUniquePaths();
@ -83,7 +83,7 @@
return;
}
assetInteraction.selectAssets(data.pathAssets);
assetInteraction.selectAssets(data.pathAssets.map((a) => toTimelineAsset(a)));
};
</script>

View File

@ -22,7 +22,7 @@
import { AssetAction } from '$lib/constants';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetStore, type TimelineAsset } from '$lib/stores/assets-store.svelte';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
import { preferences, user } from '$lib/stores/user.store';
import {
@ -42,7 +42,7 @@
void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
onDestroy(() => assetStore.destroy());
const assetInteraction = new AssetInteraction<TimelineAsset>();
const assetInteraction = new AssetInteraction();
let selectedAssets = $derived(assetInteraction.selectedAssets);
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);

View File

@ -1,28 +1,41 @@
<script lang="ts">
import { afterNavigate, goto } from '$app/navigation';
import { page } from '$app/state';
import { shortcut } from '$lib/actions/shortcut';
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import Icon from '$lib/components/elements/icon.svelte';
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.svelte';
import ArchiveAction from '$lib/components/photos-page/actions/archive-action.svelte';
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
import ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
import ChangeLocation from '$lib/components/photos-page/actions/change-location-action.svelte';
import CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
import FavoriteAction from '$lib/components/photos-page/actions/favorite-action.svelte';
import ButtonContextMenu from '$lib/components/shared-components/context-menu/button-context-menu.svelte';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.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 GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.svelte';
import { cancelMultiselect } from '$lib/utils/asset-utils';
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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { shortcut } from '$lib/actions/shortcut';
import type { TimelineAsset, Viewport } from '$lib/stores/assets-store.svelte';
import { lang, locale } from '$lib/stores/preferences.store';
import { featureFlags } from '$lib/stores/server-config.store';
import { preferences } from '$lib/stores/user.store';
import { handlePromiseError } from '$lib/utils';
import { cancelMultiselect } from '$lib/utils/asset-utils';
import { parseUtcDate } from '$lib/utils/date-time';
import { handleError } from '$lib/utils/handle-error';
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
import { toTimelineAsset } from '$lib/utils/timeline-util';
import {
type AlbumResponseDto,
type AssetResponseDto,
getPerson,
getTagById,
type MetadataSearchDto,
@ -31,21 +44,8 @@
type SmartSearchDto,
} from '@immich/sdk';
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { lang, locale } from '$lib/stores/preferences.store';
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { handlePromiseError } from '$lib/utils';
import { parseUtcDate } from '$lib/utils/date-time';
import { featureFlags } from '$lib/stores/server-config.store';
import { handleError } from '$lib/utils/handle-error';
import AlbumCardGroup from '$lib/components/album-page/album-card-group.svelte';
import { isAlbumsRoute, isPeopleRoute } from '$lib/utils/navigation';
import { t } from 'svelte-i18n';
import { tick } from 'svelte';
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
import { preferences } from '$lib/stores/user.store';
import TagAction from '$lib/components/photos-page/actions/tag-action.svelte';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { t } from 'svelte-i18n';
const MAX_ASSET_COUNT = 5000;
let { isViewing: showAssetViewer } = assetViewingStore;
@ -58,12 +58,12 @@
let nextPage = $state(1);
let searchResultAlbums: AlbumResponseDto[] = $state([]);
let searchResultAssets: AssetResponseDto[] = $state([]);
let searchResultAssets: TimelineAsset[] = $state([]);
let isLoading = $state(true);
let scrollY = $state(0);
let scrollYHistory = 0;
const assetInteraction = new AssetInteraction<AssetResponseDto>();
const assetInteraction = new AssetInteraction();
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
@ -122,7 +122,7 @@
const onAssetDelete = (assetIds: string[]) => {
const assetIdSet = new Set(assetIds);
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
searchResultAssets = searchResultAssets.filter((a: TimelineAsset) => !assetIdSet.has(a.id));
};
const handleSelectAll = () => {
assetInteraction.selectAssets(searchResultAssets);
@ -160,7 +160,7 @@
: await searchAssets({ metadataSearchDto: searchDto });
searchResultAlbums.push(...albums.items);
searchResultAssets.push(...assets.items);
searchResultAssets.push(...assets.items.map((a) => toTimelineAsset(a)));
nextPage = Number(assets.nextPage) || 0;
} catch (error) {
@ -238,7 +238,7 @@
if (terms.isNotInAlbum.toString() == 'true') {
const assetIdSet = new Set(assetIds);
searchResultAssets = searchResultAssets.filter((a: AssetResponseDto) => !assetIdSet.has(a.id));
searchResultAssets = searchResultAssets.filter((a) => !assetIdSet.has(a.id));
}
};