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;
|
onClose: (asset: AssetResponseDto) => void;
|
||||||
onNext: () => Promise<HasAsset>;
|
onNext: () => Promise<HasAsset>;
|
||||||
onPrevious: () => Promise<HasAsset>;
|
onPrevious: () => Promise<HasAsset>;
|
||||||
onRandom: () => Promise<AssetResponseDto | undefined>;
|
onRandom: () => Promise<{ id: string } | undefined>;
|
||||||
copyImage?: () => Promise<void>;
|
copyImage?: () => Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -89,7 +89,7 @@
|
|||||||
copyImage = $bindable(),
|
copyImage = $bindable(),
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
const { setAsset } = assetViewingStore;
|
const { setAssetId } = assetViewingStore;
|
||||||
const {
|
const {
|
||||||
restartProgress: restartSlideshowProgress,
|
restartProgress: restartSlideshowProgress,
|
||||||
stopProgress: stopSlideshowProgress,
|
stopProgress: stopSlideshowProgress,
|
||||||
@ -210,7 +210,7 @@
|
|||||||
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
slideshowStateUnsubscribe = slideshowState.subscribe((value) => {
|
||||||
if (value === SlideshowState.PlaySlideshow) {
|
if (value === SlideshowState.PlaySlideshow) {
|
||||||
slideshowHistory.reset();
|
slideshowHistory.reset();
|
||||||
slideshowHistory.queue(asset);
|
slideshowHistory.queue(toTimelineAsset(asset));
|
||||||
handlePromiseError(handlePlaySlideshow());
|
handlePromiseError(handlePlaySlideshow());
|
||||||
} else if (value === SlideshowState.StopSlideshow) {
|
} else if (value === SlideshowState.StopSlideshow) {
|
||||||
handlePromiseError(handleStopSlideshow());
|
handlePromiseError(handleStopSlideshow());
|
||||||
@ -220,7 +220,7 @@
|
|||||||
shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => {
|
shuffleSlideshowUnsubscribe = slideshowNavigation.subscribe((value) => {
|
||||||
if (value === SlideshowNavigation.Shuffle) {
|
if (value === SlideshowNavigation.Shuffle) {
|
||||||
slideshowHistory.reset();
|
slideshowHistory.reset();
|
||||||
slideshowHistory.queue(asset);
|
slideshowHistory.queue(toTimelineAsset(asset));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -335,8 +335,7 @@
|
|||||||
let assetViewerHtmlElement = $state<HTMLElement>();
|
let assetViewerHtmlElement = $state<HTMLElement>();
|
||||||
|
|
||||||
const slideshowHistory = new SlideshowHistory((asset) => {
|
const slideshowHistory = new SlideshowHistory((asset) => {
|
||||||
setAsset(asset);
|
handlePromiseError(setAssetId(asset.id).then(() => ($restartSlideshowProgress = true)));
|
||||||
$restartSlideshowProgress = true;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleVideoStarted = () => {
|
const handleVideoStarted = () => {
|
||||||
|
@ -26,14 +26,15 @@
|
|||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { 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 { type MemoryAsset, memoryStore } from '$lib/stores/memory.store.svelte';
|
||||||
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
import { locale, videoViewerMuted, videoViewerVolume } from '$lib/stores/preferences.store';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
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 { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
import { fromLocalDateTime } from '$lib/utils/timeline-util';
|
import { getAltText } from '$lib/utils/thumbnail-util';
|
||||||
import { AssetMediaSize, type AssetResponseDto, AssetTypeEnum } from '@immich/sdk';
|
import { fromLocalDateTime, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { AssetMediaSize, getAssetInfo } from '@immich/sdk';
|
||||||
import { IconButton } from '@immich/ui';
|
import { IconButton } from '@immich/ui';
|
||||||
import {
|
import {
|
||||||
mdiCardsOutline,
|
mdiCardsOutline,
|
||||||
@ -66,6 +67,11 @@
|
|||||||
let playerInitialized = $state(false);
|
let playerInitialized = $state(false);
|
||||||
let paused = $state(false);
|
let paused = $state(false);
|
||||||
let current = $state<MemoryAsset | undefined>(undefined);
|
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 isSaved = $derived(current?.memory.isSaved);
|
||||||
let viewerHeight = $state(0);
|
let viewerHeight = $state(0);
|
||||||
|
|
||||||
@ -73,11 +79,11 @@
|
|||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
// need to include padding in the viewport for gallery
|
// need to include padding in the viewport for gallery
|
||||||
const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
|
const galleryViewport: Viewport = $derived({ height: viewport.height, width: viewport.width - 32 });
|
||||||
const assetInteraction = new AssetInteraction<AssetResponseDto>();
|
const assetInteraction = new AssetInteraction();
|
||||||
let progressBarController: Tween<number> | undefined = $state(undefined);
|
let progressBarController: Tween<number> | undefined = $state(undefined);
|
||||||
let videoPlayer: HTMLVideoElement | undefined = $state();
|
let videoPlayer: HTMLVideoElement | undefined = $state();
|
||||||
const asHref = (asset: AssetResponseDto) => `?${QueryParameter.ID}=${asset.id}`;
|
const asHref = (asset: { id: string }) => `?${QueryParameter.ID}=${asset.id}`;
|
||||||
const handleNavigate = async (asset?: AssetResponseDto) => {
|
const handleNavigate = async (asset?: { id: string }) => {
|
||||||
if ($isViewing) {
|
if ($isViewing) {
|
||||||
return asset;
|
return asset;
|
||||||
}
|
}
|
||||||
@ -88,9 +94,9 @@
|
|||||||
|
|
||||||
await goto(asHref(asset));
|
await goto(asHref(asset));
|
||||||
};
|
};
|
||||||
const setProgressDuration = (asset: AssetResponseDto) => {
|
const setProgressDuration = (asset: TimelineAsset) => {
|
||||||
if (asset.type === AssetTypeEnum.Video) {
|
if (asset.isVideo) {
|
||||||
const timeParts = asset.duration.split(':').map(Number);
|
const timeParts = asset.duration!.split(':').map(Number);
|
||||||
const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000;
|
const durationInMilliseconds = (timeParts[0] * 3600 + timeParts[1] * 60 + timeParts[2]) * 1000;
|
||||||
progressBarController = new Tween<number>(0, {
|
progressBarController = new Tween<number>(0, {
|
||||||
duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0),
|
duration: (from: number, to: number) => (to ? durationInMilliseconds * (to - from) : 0),
|
||||||
@ -106,7 +112,8 @@
|
|||||||
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
|
const handleNextMemory = () => handleNavigate(current?.nextMemory?.assets[0]);
|
||||||
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
|
const handlePreviousMemory = () => handleNavigate(current?.previousMemory?.assets[0]);
|
||||||
const handleEscape = async () => goto(AppRoute.PHOTOS);
|
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') => {
|
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!
|
// 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}`);
|
// console.log(`handleAction[${callingContext}] called with: ${action}`);
|
||||||
@ -239,7 +246,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const initPlayer = () => {
|
const initPlayer = () => {
|
||||||
const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.type === AssetTypeEnum.Video && !videoPlayer;
|
const isVideoAssetButPlayerHasNotLoadedYet = current && current.asset.isVideo && !videoPlayer;
|
||||||
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
|
if (playerInitialized || isVideoAssetButPlayerHasNotLoadedYet) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -439,7 +446,7 @@
|
|||||||
<div class="relative h-full w-full rounded-2xl bg-black">
|
<div class="relative h-full w-full rounded-2xl bg-black">
|
||||||
{#key current.asset.id}
|
{#key current.asset.id}
|
||||||
<div transition:fade class="h-full w-full">
|
<div transition:fade class="h-full w-full">
|
||||||
{#if current.asset.type === AssetTypeEnum.Video}
|
{#if current.asset.isVideo}
|
||||||
<video
|
<video
|
||||||
bind:this={videoPlayer}
|
bind:this={videoPlayer}
|
||||||
autoplay
|
autoplay
|
||||||
@ -453,13 +460,15 @@
|
|||||||
transition:fade
|
transition:fade
|
||||||
></video>
|
></video>
|
||||||
{:else}
|
{:else}
|
||||||
<img
|
{#await currentMemoryAssetFull then asset}
|
||||||
class="h-full w-full rounded-2xl object-contain transition-all"
|
<img
|
||||||
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
|
class="h-full w-full rounded-2xl object-contain transition-all"
|
||||||
alt={current.asset.exifInfo?.description}
|
src={getAssetThumbnailUrl({ id: current.asset.id, size: AssetMediaSize.Preview })}
|
||||||
draggable="false"
|
alt={$getAltText(asset)}
|
||||||
transition:fade
|
draggable="false"
|
||||||
/>
|
transition:fade
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
{/key}
|
{/key}
|
||||||
@ -547,8 +556,10 @@
|
|||||||
{fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
|
{fromLocalDateTime(current.memory.assets[0].localDateTime).toLocaleString(DateTime.DATE_FULL)}
|
||||||
</p>
|
</p>
|
||||||
<p>
|
<p>
|
||||||
{current.asset.exifInfo?.city || ''}
|
{#await currentMemoryAssetFull then asset}
|
||||||
{current.asset.exifInfo?.country || ''}
|
{asset?.exifInfo?.city || ''}
|
||||||
|
{asset?.exifInfo?.country || ''}
|
||||||
|
{/await}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@ -619,7 +630,7 @@
|
|||||||
<GalleryViewer
|
<GalleryViewer
|
||||||
onNext={handleNextAsset}
|
onNext={handleNextAsset}
|
||||||
onPrevious={handlePreviousAsset}
|
onPrevious={handlePreviousAsset}
|
||||||
assets={current.memory.assets}
|
assets={currentTimelineAssets}
|
||||||
viewport={galleryViewport}
|
viewport={galleryViewport}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
slidingWindowOffset={viewerHeight}
|
slidingWindowOffset={viewerHeight}
|
||||||
|
@ -1,27 +1,27 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from '$app/navigation';
|
||||||
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
import type { Action } from '$lib/components/asset-viewer/actions/action';
|
||||||
|
import ImmichLogoSmallLink from '$lib/components/shared-components/immich-logo-small-link.svelte';
|
||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
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 { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
|
||||||
import { getKey, handlePromiseError } from '$lib/utils';
|
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 { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
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 { 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 CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
|
||||||
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
import DownloadAction from '../photos-page/actions/download-action.svelte';
|
||||||
import RemoveFromSharedLink from '../photos-page/actions/remove-from-shared-link.svelte';
|
import RemoveFromSharedLink from '../photos-page/actions/remove-from-shared-link.svelte';
|
||||||
import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte';
|
import AssetSelectControlBar from '../photos-page/asset-select-control-bar.svelte';
|
||||||
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
import ControlAppBar from '../shared-components/control-app-bar.svelte';
|
||||||
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
import GalleryViewer from '../shared-components/gallery-viewer/gallery-viewer.svelte';
|
||||||
import 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 { 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 {
|
interface Props {
|
||||||
sharedLink: SharedLinkResponseDto;
|
sharedLink: SharedLinkResponseDto;
|
||||||
@ -31,9 +31,10 @@
|
|||||||
let { sharedLink = $bindable(), isOwned }: Props = $props();
|
let { sharedLink = $bindable(), isOwned }: Props = $props();
|
||||||
|
|
||||||
const viewport: Viewport = $state({ width: 0, height: 0 });
|
const viewport: Viewport = $state({ width: 0, height: 0 });
|
||||||
const assetInteraction = new AssetInteraction<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) => {
|
dragAndDropFilesStore.subscribe((value) => {
|
||||||
if (value.isDragging && value.files.length > 0) {
|
if (value.isDragging && value.files.length > 0) {
|
||||||
@ -127,14 +128,16 @@
|
|||||||
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
<GalleryViewer {assets} {assetInteraction} {viewport} />
|
||||||
</section>
|
</section>
|
||||||
{:else}
|
{:else}
|
||||||
<AssetViewer
|
{#await fullAsset then asset}
|
||||||
asset={assets[0]}
|
<AssetViewer
|
||||||
showCloseButton={false}
|
asset={asset!}
|
||||||
onAction={handleAction}
|
showCloseButton={false}
|
||||||
onPrevious={() => Promise.resolve(false)}
|
onAction={handleAction}
|
||||||
onNext={() => Promise.resolve(false)}
|
onPrevious={() => Promise.resolve(false)}
|
||||||
onRandom={() => Promise.resolve(undefined)}
|
onNext={() => Promise.resolve(false)}
|
||||||
onClose={() => {}}
|
onRandom={() => Promise.resolve(undefined)}
|
||||||
/>
|
onClose={() => {}}
|
||||||
|
/>
|
||||||
|
{/await}
|
||||||
{/if}
|
{/if}
|
||||||
</section>
|
</section>
|
||||||
|
@ -6,7 +6,7 @@
|
|||||||
import { AppRoute, AssetAction } from '$lib/constants';
|
import { AppRoute, AssetAction } from '$lib/constants';
|
||||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { 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 { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { featureFlags } from '$lib/stores/server-config.store';
|
import { featureFlags } from '$lib/stores/server-config.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
@ -25,17 +25,17 @@
|
|||||||
import ShowShortcuts from '../show-shortcuts.svelte';
|
import ShowShortcuts from '../show-shortcuts.svelte';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
assets: AssetResponseDto[];
|
assets: (TimelineAsset | AssetResponseDto)[];
|
||||||
assetInteraction: AssetInteraction<AssetResponseDto>;
|
assetInteraction: AssetInteraction;
|
||||||
disableAssetSelect?: boolean;
|
disableAssetSelect?: boolean;
|
||||||
showArchiveIcon?: boolean;
|
showArchiveIcon?: boolean;
|
||||||
viewport: Viewport;
|
viewport: Viewport;
|
||||||
onIntersected?: (() => void) | undefined;
|
onIntersected?: (() => void) | undefined;
|
||||||
showAssetName?: boolean;
|
showAssetName?: boolean;
|
||||||
isShowDeleteConfirmation?: boolean;
|
isShowDeleteConfirmation?: boolean;
|
||||||
onPrevious?: (() => Promise<AssetResponseDto | undefined>) | undefined;
|
onPrevious?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
onNext?: (() => Promise<AssetResponseDto | undefined>) | undefined;
|
onNext?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
onRandom?: (() => Promise<AssetResponseDto | undefined>) | undefined;
|
onRandom?: (() => Promise<{ id: string } | undefined>) | undefined;
|
||||||
pageHeaderOffset?: number;
|
pageHeaderOffset?: number;
|
||||||
slidingWindowOffset?: number;
|
slidingWindowOffset?: number;
|
||||||
}
|
}
|
||||||
@ -56,7 +56,7 @@
|
|||||||
pageHeaderOffset = 0,
|
pageHeaderOffset = 0,
|
||||||
}: Props = $props();
|
}: Props = $props();
|
||||||
|
|
||||||
let { isViewing: isViewerOpen, asset: viewingAsset, setAsset } = assetViewingStore;
|
let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
|
||||||
|
|
||||||
let geometry: CommonJustifiedLayout | undefined = $state();
|
let geometry: CommonJustifiedLayout | undefined = $state();
|
||||||
|
|
||||||
@ -83,19 +83,26 @@
|
|||||||
containerHeight = geometry.containerHeight;
|
containerHeight = geometry.containerHeight;
|
||||||
containerWidth = geometry.containerWidth;
|
containerWidth = geometry.containerWidth;
|
||||||
for (const [i, asset] of assets.entries()) {
|
for (const [i, asset] of assets.entries()) {
|
||||||
const layout = {
|
const top = geometry.getTop(i);
|
||||||
asset,
|
const left = geometry.getLeft(i);
|
||||||
top: geometry.getTop(i),
|
const width = geometry.getWidth(i);
|
||||||
left: geometry.getLeft(i),
|
const height = geometry.getHeight(i);
|
||||||
width: geometry.getWidth(i),
|
|
||||||
height: geometry.getHeight(i),
|
const layoutTopWithOffset = top + pageHeaderOffset;
|
||||||
};
|
const layoutBottom = layoutTopWithOffset + height;
|
||||||
// 54 is the content height of the asset-selection-app-bar
|
|
||||||
const layoutTopWithOffset = layout.top + pageHeaderOffset;
|
|
||||||
const layoutBottom = layoutTopWithOffset + layout.height;
|
|
||||||
|
|
||||||
const display = layoutTopWithOffset < slidingWindow.bottom && layoutBottom > slidingWindow.top;
|
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 showShortcuts = $state(false);
|
||||||
let currentViewAssetIndex = 0;
|
let currentViewAssetIndex = 0;
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
let lastAssetMouseEvent: AssetResponseDto | null = $state(null);
|
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
|
||||||
let slidingWindow = $state({ top: 0, bottom: 0 });
|
let slidingWindow = $state({ top: 0, bottom: 0 });
|
||||||
|
|
||||||
const updateSlidingWindow = () => {
|
const updateSlidingWindow = () => {
|
||||||
@ -139,14 +146,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const viewAssetHandler = async (asset: AssetResponseDto) => {
|
const viewAssetHandler = async (asset: TimelineAsset) => {
|
||||||
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
currentViewAssetIndex = assets.findIndex((a) => a.id == asset.id);
|
||||||
setAsset(assets[currentViewAssetIndex]);
|
await setAssetId(assets[currentViewAssetIndex].id);
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAllAssets = () => {
|
const selectAllAssets = () => {
|
||||||
assetInteraction.selectAssets(assets);
|
assetInteraction.selectAssets(assets.map((a) => toTimelineAsset(a)));
|
||||||
};
|
};
|
||||||
|
|
||||||
const deselectAllAssets = () => {
|
const deselectAllAssets = () => {
|
||||||
@ -168,7 +175,7 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssets = (asset: AssetResponseDto) => {
|
const handleSelectAssets = (asset: TimelineAsset) => {
|
||||||
if (!asset) {
|
if (!asset) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -191,14 +198,14 @@
|
|||||||
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
assetInteraction.setAssetSelectionStart(deselect ? null : asset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleSelectAssetCandidates = (asset: AssetResponseDto | null) => {
|
const handleSelectAssetCandidates = (asset: TimelineAsset | null) => {
|
||||||
if (asset) {
|
if (asset) {
|
||||||
selectAssetCandidates(asset);
|
selectAssetCandidates(asset);
|
||||||
}
|
}
|
||||||
lastAssetMouseEvent = asset;
|
lastAssetMouseEvent = asset;
|
||||||
};
|
};
|
||||||
|
|
||||||
const selectAssetCandidates = (endAsset: AssetResponseDto) => {
|
const selectAssetCandidates = (endAsset: TimelineAsset) => {
|
||||||
if (!shiftKeyIsDown) {
|
if (!shiftKeyIsDown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@ -215,7 +222,7 @@
|
|||||||
[start, end] = [end, start];
|
[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) => {
|
const onSelectStart = (e: Event) => {
|
||||||
@ -310,7 +317,7 @@
|
|||||||
|
|
||||||
const handleNext = async (): Promise<boolean> => {
|
const handleNext = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
let asset: AssetResponseDto | undefined;
|
let asset: { id: string } | undefined;
|
||||||
if (onNext) {
|
if (onNext) {
|
||||||
asset = await onNext();
|
asset = await onNext();
|
||||||
} else {
|
} else {
|
||||||
@ -334,9 +341,9 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleRandom = async (): Promise<AssetResponseDto | undefined> => {
|
const handleRandom = async (): Promise<{ id: string } | undefined> => {
|
||||||
try {
|
try {
|
||||||
let asset: AssetResponseDto | undefined;
|
let asset: { id: string } | undefined;
|
||||||
if (onRandom) {
|
if (onRandom) {
|
||||||
asset = await onRandom();
|
asset = await onRandom();
|
||||||
} else {
|
} else {
|
||||||
@ -360,7 +367,7 @@
|
|||||||
|
|
||||||
const handlePrevious = async (): Promise<boolean> => {
|
const handlePrevious = async (): Promise<boolean> => {
|
||||||
try {
|
try {
|
||||||
let asset: AssetResponseDto | undefined;
|
let asset: { id: string } | undefined;
|
||||||
if (onPrevious) {
|
if (onPrevious) {
|
||||||
asset = await onPrevious();
|
asset = await onPrevious();
|
||||||
} else {
|
} else {
|
||||||
@ -384,9 +391,9 @@
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const navigateToAsset = async (asset?: AssetResponseDto) => {
|
const navigateToAsset = async (asset?: { id: string }) => {
|
||||||
if (asset && asset.id !== $viewingAsset.id) {
|
if (asset && asset.id !== $viewingAsset.id) {
|
||||||
setAsset(asset);
|
await setAssetId(asset.id);
|
||||||
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
await navigate({ targetRoute: 'current', assetId: $viewingAsset.id });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -405,20 +412,20 @@
|
|||||||
} else if (currentViewAssetIndex === assets.length) {
|
} else if (currentViewAssetIndex === assets.length) {
|
||||||
await handlePrevious();
|
await handlePrevious();
|
||||||
} else {
|
} else {
|
||||||
setAsset(assets[currentViewAssetIndex]);
|
await setAssetId(assets[currentViewAssetIndex].id);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const assetMouseEventHandler = (asset: AssetResponseDto | null) => {
|
const assetMouseEventHandler = (asset: TimelineAsset | null) => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
handleSelectAssetCandidates(asset);
|
handleSelectAssetCandidates(asset);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const assetOnFocusHandler = (asset: AssetResponseDto) => {
|
const assetOnFocusHandler = (asset: TimelineAsset) => {
|
||||||
assetInteraction.focussedAssetId = asset.id;
|
assetInteraction.focussedAssetId = asset.id;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -478,20 +485,19 @@
|
|||||||
class="absolute"
|
class="absolute"
|
||||||
style:overflow="clip"
|
style:overflow="clip"
|
||||||
style="width: {layout.width}px; height: {layout.height}px; top: {layout.top}px; left: {layout.left}px"
|
style="width: {layout.width}px; height: {layout.height}px; top: {layout.top}px; left: {layout.left}px"
|
||||||
title={showAssetName ? asset.originalFileName : ''}
|
|
||||||
>
|
>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
readonly={disableAssetSelect}
|
readonly={disableAssetSelect}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (assetInteraction.selectionActive) {
|
if (assetInteraction.selectionActive) {
|
||||||
handleSelectAssets(asset);
|
handleSelectAssets(toTimelineAsset(asset));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
void viewAssetHandler(asset);
|
void viewAssetHandler(toTimelineAsset(asset));
|
||||||
}}
|
}}
|
||||||
onSelect={() => handleSelectAssets(asset)}
|
onSelect={() => handleSelectAssets(toTimelineAsset(asset))}
|
||||||
onMouseEvent={() => assetMouseEventHandler(asset)}
|
onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(asset))}
|
||||||
handleFocus={() => assetOnFocusHandler(asset)}
|
handleFocus={() => assetOnFocusHandler(toTimelineAsset(asset))}
|
||||||
{showArchiveIcon}
|
{showArchiveIcon}
|
||||||
asset={toTimelineAsset(asset)}
|
asset={toTimelineAsset(asset)}
|
||||||
selected={assetInteraction.hasSelectedAsset(asset.id)}
|
selected={assetInteraction.hasSelectedAsset(asset.id)}
|
||||||
@ -500,11 +506,13 @@
|
|||||||
thumbnailWidth={layout.width}
|
thumbnailWidth={layout.width}
|
||||||
thumbnailHeight={layout.height}
|
thumbnailHeight={layout.height}
|
||||||
/>
|
/>
|
||||||
|
<!-- note: if using showAssetName then the 'assets' prop must be AssetResponseDto (only used by folders) -->
|
||||||
{#if showAssetName}
|
{#if showAssetName}
|
||||||
<div
|
<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"
|
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>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,27 +1,20 @@
|
|||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { user } from '$lib/stores/user.store';
|
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 { SvelteSet } from 'svelte/reactivity';
|
||||||
import { fromStore } from 'svelte/store';
|
import { fromStore } from 'svelte/store';
|
||||||
|
|
||||||
export type BaseInteractionAsset = {
|
export class AssetInteraction {
|
||||||
id: string;
|
selectedAssets = $state<TimelineAsset[]>([]);
|
||||||
isTrashed: boolean;
|
|
||||||
isArchived: boolean;
|
|
||||||
isFavorite: boolean;
|
|
||||||
ownerId: string;
|
|
||||||
stack?: AssetStackResponseDto | null | undefined;
|
|
||||||
};
|
|
||||||
export class AssetInteraction<T extends BaseInteractionAsset> {
|
|
||||||
selectedAssets = $state<T[]>([]);
|
|
||||||
hasSelectedAsset(assetId: string) {
|
hasSelectedAsset(assetId: string) {
|
||||||
return this.selectedAssets.some((asset) => asset.id === assetId);
|
return this.selectedAssets.some((asset) => asset.id === assetId);
|
||||||
}
|
}
|
||||||
selectedGroup = new SvelteSet<string>();
|
selectedGroup = new SvelteSet<string>();
|
||||||
assetSelectionCandidates = $state<T[]>([]);
|
assetSelectionCandidates = $state<TimelineAsset[]>([]);
|
||||||
hasSelectionCandidate(assetId: string) {
|
hasSelectionCandidate(assetId: string) {
|
||||||
return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
|
return this.assetSelectionCandidates.some((asset) => asset.id === assetId);
|
||||||
}
|
}
|
||||||
assetSelectionStart = $state<T | null>(null);
|
assetSelectionStart = $state<TimelineAsset | null>(null);
|
||||||
focussedAssetId = $state<string | null>(null);
|
focussedAssetId = $state<string | null>(null);
|
||||||
selectionActive = $derived(this.selectedAssets.length > 0);
|
selectionActive = $derived(this.selectedAssets.length > 0);
|
||||||
|
|
||||||
@ -33,13 +26,13 @@ export class AssetInteraction<T extends BaseInteractionAsset> {
|
|||||||
isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
|
isAllFavorite = $derived(this.selectedAssets.every((asset) => asset.isFavorite));
|
||||||
isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
|
isAllUserOwned = $derived(this.selectedAssets.every((asset) => asset.ownerId === this.userId));
|
||||||
|
|
||||||
selectAsset(asset: T) {
|
selectAsset(asset: TimelineAsset) {
|
||||||
if (!this.hasSelectedAsset(asset.id)) {
|
if (!this.hasSelectedAsset(asset.id)) {
|
||||||
this.selectedAssets.push(asset);
|
this.selectedAssets.push(asset);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
selectAssets(assets: T[]) {
|
selectAssets(assets: TimelineAsset[]) {
|
||||||
for (const asset of assets) {
|
for (const asset of assets) {
|
||||||
this.selectAsset(asset);
|
this.selectAsset(asset);
|
||||||
}
|
}
|
||||||
@ -60,11 +53,11 @@ export class AssetInteraction<T extends BaseInteractionAsset> {
|
|||||||
this.selectedGroup.delete(group);
|
this.selectedGroup.delete(group);
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssetSelectionStart(asset: T | null) {
|
setAssetSelectionStart(asset: TimelineAsset | null) {
|
||||||
this.assetSelectionStart = asset;
|
this.assetSelectionStart = asset;
|
||||||
}
|
}
|
||||||
|
|
||||||
setAssetSelectionCandidates(assets: T[]) {
|
setAssetSelectionCandidates(assets: TimelineAsset[]) {
|
||||||
this.assetSelectionCandidates = assets;
|
this.assetSelectionCandidates = assets;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,12 +1,7 @@
|
|||||||
|
import type { TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { asLocalTimeISO } from '$lib/utils/date-time';
|
import { asLocalTimeISO } from '$lib/utils/date-time';
|
||||||
import {
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
type AssetResponseDto,
|
import { deleteMemory, type MemoryResponseDto, removeMemoryAssets, searchMemories, updateMemory } from '@immich/sdk';
|
||||||
deleteMemory,
|
|
||||||
type MemoryResponseDto,
|
|
||||||
removeMemoryAssets,
|
|
||||||
searchMemories,
|
|
||||||
updateMemory,
|
|
||||||
} from '@immich/sdk';
|
|
||||||
import { DateTime } from 'luxon';
|
import { DateTime } from 'luxon';
|
||||||
|
|
||||||
type MemoryIndex = {
|
type MemoryIndex = {
|
||||||
@ -16,7 +11,7 @@ type MemoryIndex = {
|
|||||||
|
|
||||||
export type MemoryAsset = MemoryIndex & {
|
export type MemoryAsset = MemoryIndex & {
|
||||||
memory: MemoryResponseDto;
|
memory: MemoryResponseDto;
|
||||||
asset: AssetResponseDto;
|
asset: TimelineAsset;
|
||||||
previousMemory?: MemoryResponseDto;
|
previousMemory?: MemoryResponseDto;
|
||||||
previous?: MemoryAsset;
|
previous?: MemoryAsset;
|
||||||
next?: MemoryAsset;
|
next?: MemoryAsset;
|
||||||
@ -36,7 +31,7 @@ class MemoryStoreSvelte {
|
|||||||
memoryIndex,
|
memoryIndex,
|
||||||
previousMemory: this.memories[memoryIndex - 1],
|
previousMemory: this.memories[memoryIndex - 1],
|
||||||
nextMemory: this.memories[memoryIndex + 1],
|
nextMemory: this.memories[memoryIndex + 1],
|
||||||
asset,
|
asset: toTimelineAsset(asset),
|
||||||
assetIndex,
|
assetIndex,
|
||||||
previous,
|
previous,
|
||||||
};
|
};
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import type { AssetResponseDto } from '@immich/sdk';
|
|
||||||
|
|
||||||
export class SlideshowHistory {
|
export class SlideshowHistory {
|
||||||
private history: AssetResponseDto[] = [];
|
private history: { id: string }[] = [];
|
||||||
private index = 0;
|
private index = 0;
|
||||||
|
|
||||||
constructor(private onChange: (asset: AssetResponseDto) => void) {}
|
constructor(private onChange: (asset: { id: string }) => void) {}
|
||||||
|
|
||||||
reset() {
|
reset() {
|
||||||
this.history = [];
|
this.history = [];
|
||||||
this.index = 0;
|
this.index = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
queue(asset: AssetResponseDto) {
|
queue(asset: { id: string }) {
|
||||||
this.history.push(asset);
|
this.history.push(asset);
|
||||||
|
|
||||||
// If we were at the end of the slideshow history, move the index to the new end
|
// 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;
|
return 300;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const getAltTextForTimelineAsset = () => {
|
||||||
|
// TODO: implement this in a performant way
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
export const getAltText = derived(t, ($t) => {
|
export const getAltText = derived(t, ($t) => {
|
||||||
return (asset: AssetResponseDto) => {
|
return (asset: AssetResponseDto | null | undefined) => {
|
||||||
|
if (!asset) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
if (asset.exifInfo?.description) {
|
if (asset.exifInfo?.description) {
|
||||||
return 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 type { AssetBucket, TimelineAsset } from '$lib/stores/assets-store.svelte';
|
||||||
import { locale } from '$lib/stores/preferences.store';
|
import { locale } from '$lib/stores/preferences.store';
|
||||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
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 formatDateGroupTitle = memoize(formatGroupTitle);
|
||||||
|
|
||||||
export const toTimelineAsset = (unknownAsset: BaseInteractionAsset): TimelineAsset => {
|
export const toTimelineAsset = (unknownAsset: AssetResponseDto | TimelineAsset): TimelineAsset => {
|
||||||
if (isTimelineAsset(unknownAsset)) {
|
if (isTimelineAsset(unknownAsset)) {
|
||||||
return unknownAsset;
|
return unknownAsset;
|
||||||
}
|
}
|
||||||
@ -132,5 +131,5 @@ export const toTimelineAsset = (unknownAsset: BaseInteractionAsset): TimelineAss
|
|||||||
livePhotoVideoId: assetResponse.livePhotoVideoId || null,
|
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;
|
(arg as TimelineAsset).ratio !== undefined;
|
||||||
|
@ -27,8 +27,8 @@
|
|||||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||||
import { preferences } from '$lib/stores/user.store';
|
import { preferences } from '$lib/stores/user.store';
|
||||||
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
import { cancelMultiselect } from '$lib/utils/asset-utils';
|
||||||
|
import { toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
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 { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
@ -47,7 +47,7 @@
|
|||||||
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
let currentPath = $derived($page.url.searchParams.get(QueryParameter.PATH) || '');
|
||||||
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
|
let currentTreeItems = $derived(currentPath ? data.currentFolders : Object.keys(tree).sort());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction<AssetResponseDto>();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
onMount(async () => {
|
onMount(async () => {
|
||||||
await foldersStore.fetchUniquePaths();
|
await foldersStore.fetchUniquePaths();
|
||||||
@ -83,7 +83,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
assetInteraction.selectAssets(data.pathAssets);
|
assetInteraction.selectAssets(data.pathAssets.map((a) => toTimelineAsset(a)));
|
||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
@ -22,7 +22,7 @@
|
|||||||
import { AssetAction } from '$lib/constants';
|
import { AssetAction } from '$lib/constants';
|
||||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { 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 { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||||
import { preferences, user } from '$lib/stores/user.store';
|
import { preferences, user } from '$lib/stores/user.store';
|
||||||
import {
|
import {
|
||||||
@ -42,7 +42,7 @@
|
|||||||
void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
|
void assetStore.updateOptions({ isArchived: false, withStacked: true, withPartners: true });
|
||||||
onDestroy(() => assetStore.destroy());
|
onDestroy(() => assetStore.destroy());
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction<TimelineAsset>();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
let selectedAssets = $derived(assetInteraction.selectedAssets);
|
let selectedAssets = $derived(assetInteraction.selectedAssets);
|
||||||
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
|
let isAssetStackSelected = $derived(selectedAssets.length === 1 && !!selectedAssets[0].stack);
|
||||||
|
@ -1,28 +1,41 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { afterNavigate, goto } from '$app/navigation';
|
import { afterNavigate, goto } from '$app/navigation';
|
||||||
import { page } from '$app/state';
|
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 CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
|
||||||
import Icon from '$lib/components/elements/icon.svelte';
|
import Icon from '$lib/components/elements/icon.svelte';
|
||||||
import AddToAlbum from '$lib/components/photos-page/actions/add-to-album.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 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 ChangeDate from '$lib/components/photos-page/actions/change-date-action.svelte';
|
||||||
import ChangeLocation from '$lib/components/photos-page/actions/change-location-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 CreateSharedLink from '$lib/components/photos-page/actions/create-shared-link.svelte';
|
||||||
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
import DeleteAssets from '$lib/components/photos-page/actions/delete-assets.svelte';
|
||||||
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
import DownloadAction from '$lib/components/photos-page/actions/download-action.svelte';
|
||||||
import FavoriteAction from '$lib/components/photos-page/actions/favorite-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 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 ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||||
import GalleryViewer from '$lib/components/shared-components/gallery-viewer/gallery-viewer.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 SearchBar from '$lib/components/shared-components/search-bar/search-bar.svelte';
|
||||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||||
|
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
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 {
|
import {
|
||||||
type AlbumResponseDto,
|
type AlbumResponseDto,
|
||||||
type AssetResponseDto,
|
|
||||||
getPerson,
|
getPerson,
|
||||||
getTagById,
|
getTagById,
|
||||||
type MetadataSearchDto,
|
type MetadataSearchDto,
|
||||||
@ -31,21 +44,8 @@
|
|||||||
type SmartSearchDto,
|
type SmartSearchDto,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
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 { tick } from 'svelte';
|
||||||
import AssetJobActions from '$lib/components/photos-page/actions/asset-job-actions.svelte';
|
import { t } from 'svelte-i18n';
|
||||||
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';
|
|
||||||
|
|
||||||
const MAX_ASSET_COUNT = 5000;
|
const MAX_ASSET_COUNT = 5000;
|
||||||
let { isViewing: showAssetViewer } = assetViewingStore;
|
let { isViewing: showAssetViewer } = assetViewingStore;
|
||||||
@ -58,12 +58,12 @@
|
|||||||
|
|
||||||
let nextPage = $state(1);
|
let nextPage = $state(1);
|
||||||
let searchResultAlbums: AlbumResponseDto[] = $state([]);
|
let searchResultAlbums: AlbumResponseDto[] = $state([]);
|
||||||
let searchResultAssets: AssetResponseDto[] = $state([]);
|
let searchResultAssets: TimelineAsset[] = $state([]);
|
||||||
let isLoading = $state(true);
|
let isLoading = $state(true);
|
||||||
let scrollY = $state(0);
|
let scrollY = $state(0);
|
||||||
let scrollYHistory = 0;
|
let scrollYHistory = 0;
|
||||||
|
|
||||||
const assetInteraction = new AssetInteraction<AssetResponseDto>();
|
const assetInteraction = new AssetInteraction();
|
||||||
|
|
||||||
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
|
type SearchTerms = MetadataSearchDto & Pick<SmartSearchDto, 'query'>;
|
||||||
let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
|
let searchQuery = $derived(page.url.searchParams.get(QueryParameter.QUERY));
|
||||||
@ -122,7 +122,7 @@
|
|||||||
|
|
||||||
const onAssetDelete = (assetIds: string[]) => {
|
const onAssetDelete = (assetIds: string[]) => {
|
||||||
const assetIdSet = new Set(assetIds);
|
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 = () => {
|
const handleSelectAll = () => {
|
||||||
assetInteraction.selectAssets(searchResultAssets);
|
assetInteraction.selectAssets(searchResultAssets);
|
||||||
@ -160,7 +160,7 @@
|
|||||||
: await searchAssets({ metadataSearchDto: searchDto });
|
: await searchAssets({ metadataSearchDto: searchDto });
|
||||||
|
|
||||||
searchResultAlbums.push(...albums.items);
|
searchResultAlbums.push(...albums.items);
|
||||||
searchResultAssets.push(...assets.items);
|
searchResultAssets.push(...assets.items.map((a) => toTimelineAsset(a)));
|
||||||
|
|
||||||
nextPage = Number(assets.nextPage) || 0;
|
nextPage = Number(assets.nextPage) || 0;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -238,7 +238,7 @@
|
|||||||
|
|
||||||
if (terms.isNotInAlbum.toString() == 'true') {
|
if (terms.isNotInAlbum.toString() == 'true') {
|
||||||
const assetIdSet = new Set(assetIds);
|
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