mirror of
https://github.com/immich-app/immich.git
synced 2025-07-09 03:04:16 -04:00
GalleryViewer
This commit is contained in:
parent
3b9490e28d
commit
c1e699ebaf
@ -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 = () => {
|
||||
|
@ -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}
|
||||
{#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={current.asset.exifInfo?.description}
|
||||
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}
|
||||
|
@ -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,8 +128,9 @@
|
||||
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
||||
</section>
|
||||
{:else}
|
||||
{#await fullAsset then asset}
|
||||
<AssetViewer
|
||||
asset={assets[0]}
|
||||
asset={asset!}
|
||||
showCloseButton={false}
|
||||
onAction={handleAction}
|
||||
onPrevious={() => Promise.resolve(false)}
|
||||
@ -136,5 +138,6 @@
|
||||
onRandom={() => Promise.resolve(undefined)}
|
||||
onClose={() => {}}
|
||||
/>
|
||||
{/await}
|
||||
{/if}
|
||||
</section>
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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,
|
||||
};
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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));
|
||||
}
|
||||
};
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user