fix(web): fix lost scrollpos on deep link to timeline asset, scrub stop (#16305)

* Work in progress - super quick asset store->state

* bugfix: deep linking to timeline, on scrub stop

* format, remove stale

* disable test, todo: fix test

* remove unused import

* Fix merge

* lint

* lint

* lint

* Default to non-wasm layout

* lint

* intobs fix

* fix rejected promise

* Review comments, static import wasm

* Back to dynamic

* try top-level-await

* back to the first solution, with more finesse

* comment out wasm for now

* back out the wasm/thumbhash/thumbnail changes

* lint

* Fully remove wasm

* lockfile

---------

Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
Min Idzelis
2025-03-04 21:34:53 -05:00
committed by GitHub
parent 8b43066632
commit 56b85f7479
36 changed files with 362 additions and 305 deletions

View File

@@ -4,7 +4,7 @@
import { dragAndDropFilesStore } from '$lib/stores/drag-and-drop-files.store';
import { fileUploadHandler, openFileUploadDialog } from '$lib/utils/file-uploader';
import type { AlbumResponseDto, SharedLinkResponseDto, UserResponseDto } from '@immich/sdk';
import { AssetStore } from '$lib/stores/assets.store';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { cancelMultiselect, downloadAlbum } from '$lib/utils/asset-utils';
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import DownloadAction from '../photos-page/actions/download-action.svelte';

View File

@@ -19,7 +19,7 @@
import { NotificationType, notificationController } from '../shared-components/notification/notification';
import { handleError } from '$lib/utils/handle-error';
import FaceEditor from '$lib/components/asset-viewer/face-editor/face-editor.svelte';
import { photoViewerImgElement } from '$lib/stores/assets.store';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
interface Props {

View File

@@ -22,7 +22,7 @@
import ImageThumbnail from './image-thumbnail.svelte';
import VideoThumbnail from './video-thumbnail.svelte';
import { currentUrlReplaceAssetId } from '$lib/utils/navigation';
import { AssetStore } from '$lib/stores/assets.store';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import type { DateGroup } from '$lib/utils/timeline-util';

View File

@@ -3,7 +3,7 @@
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
import { mdiAlertCircleOutline, mdiPauseCircleOutline, mdiPlayCircleOutline } from '@mdi/js';
import Icon from '$lib/components/elements/icon.svelte';
import { AssetStore } from '$lib/stores/assets.store';
import { AssetStore } from '$lib/stores/assets-store.svelte';
import { generateId } from '$lib/utils/generate-id';
import { onDestroy } from 'svelte';

View File

@@ -6,7 +6,7 @@
import { mdiArrowLeftThin, mdiClose, mdiMagnify, mdiPlus } from '@mdi/js';
import { linear } from 'svelte/easing';
import { fly } from 'svelte/transition';
import { photoViewerImgElement } from '$lib/stores/assets.store';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import ImageThumbnail from '../assets/thumbnail/image-thumbnail.svelte';
import LoadingSpinner from '../shared-components/loading-spinner.svelte';
import SearchPeople from '$lib/components/faces-page/people-search.svelte';

View File

@@ -25,7 +25,7 @@
import AssignFaceSidePanel from './assign-face-side-panel.svelte';
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { zoomImageToBase64 } from '$lib/utils/people-utils';
import { photoViewerImgElement } from '$lib/stores/assets.store';
import { photoViewerImgElement } from '$lib/stores/assets-store.svelte';
import { t } from 'svelte-i18n';
import { dialogController } from '$lib/components/shared-components/dialog/dialog';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';

View File

@@ -26,7 +26,7 @@
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';
import { type Viewport } from '$lib/stores/assets-store.svelte';
import { loadMemories, memoryStore } from '$lib/stores/memory.store';
import { locale, videoViewerMuted } from '$lib/stores/preferences.store';
import { preferences } from '$lib/stores/user.store';

View File

@@ -8,7 +8,7 @@
import { t } from 'svelte-i18n';
interface Props {
onArchive: OnArchive;
onArchive?: OnArchive;
menuItem?: boolean;
unarchive?: boolean;
}
@@ -28,7 +28,7 @@
loading = true;
const ids = await archiveAssets(assets, isArchived);
if (ids) {
onArchive(ids, isArchived);
onArchive?.(ids, isArchived);
clearSelect();
}
loading = false;

View File

@@ -13,7 +13,7 @@
import { t } from 'svelte-i18n';
interface Props {
onFavorite: OnFavorite;
onFavorite?: OnFavorite;
menuItem?: boolean;
removeFavorite: boolean;
}
@@ -44,7 +44,7 @@
asset.isFavorite = isFavorite;
}
onFavorite(ids, isFavorite);
onFavorite?.(ids, isFavorite);
notificationController.show({
message: isFavorite

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import CircleIconButton from '$lib/components/elements/buttons/circle-icon-button.svelte';
import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets.store';
import { type AssetStore, isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
import { mdiSelectAll, mdiSelectRemove } from '@mdi/js';
import { selectAllAssets, cancelMultiselect } from '$lib/utils/asset-utils';
import { t } from 'svelte-i18n';

View File

@@ -2,7 +2,7 @@
import { intersectionObserver } from '$lib/actions/intersection-observer';
import Icon from '$lib/components/elements/icon.svelte';
import Skeleton from '$lib/components/photos-page/skeleton.svelte';
import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets.store';
import { AssetBucket, type AssetStore, type Viewport } from '$lib/stores/assets-store.svelte';
import { navigate } from '$lib/utils/navigation';
import { findTotalOffset, type DateGroup, type ScrollTargetListener } from '$lib/utils/timeline-util';
import type { AssetResponseDto } from '@immich/sdk';
@@ -89,25 +89,26 @@
};
onDestroy(() => {
$assetStore.taskManager.removeAllTasksForComponent(componentId);
assetStore.taskManager.removeAllTasksForComponent(componentId);
});
</script>
<section id="asset-group-by-date" class="flex flex-wrap gap-x-12" data-bucket-date={bucketDate} bind:this={element}>
{#each dateGroups as dateGroup, groupIndex (dateGroup.date)}
{@const display =
dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === $assetStore.pendingScrollAssetId)}
dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === assetStore.pendingScrollAssetId)}
{@const geometry = dateGroup.geometry!}
<div
id="date-group"
use:intersectionObserver={{
onIntersect: () => {
$assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () =>
assetStore.taskManager.intersectedDateGroup(componentId, dateGroup, () =>
assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: true }),
);
},
onSeparate: () => {
$assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () =>
assetStore.taskManager.separatedDateGroup(componentId, dateGroup, () =>
assetStore.updateBucketDateGroup(bucket, dateGroup, { intersecting: false }),
);
},
@@ -118,7 +119,7 @@
data-display={display}
data-date-group={dateGroup.date}
style:height={dateGroup.height + 'px'}
style:width={dateGroup.geometry.containerWidth + 'px'}
style:width={geometry.containerWidth + 'px'}
style:overflow="clip"
>
{#if !display}
@@ -129,7 +130,7 @@
<!-- svelte-ignore a11y-no-static-element-interactions -->
<div
on:mouseenter={() =>
$assetStore.taskManager.queueScrollSensitiveTask({
assetStore.taskManager.queueScrollSensitiveTask({
componentId,
task: () => {
isMouseOverGroup = true;
@@ -137,7 +138,7 @@
},
})}
on:mouseleave={() => {
$assetStore.taskManager.queueScrollSensitiveTask({
assetStore.taskManager.queueScrollSensitiveTask({
componentId,
task: () => {
isMouseOverGroup = false;
@@ -149,7 +150,7 @@
<!-- Date group title -->
<div
class="flex z-[100] sticky top-[-1px] pt-[calc(1.75rem+1px)] pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
style:width={dateGroup.geometry.containerWidth + 'px'}
style:width={geometry.containerWidth + 'px'}
>
{#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
<div
@@ -174,11 +175,15 @@
<!-- Image grid -->
<div
class="relative overflow-clip"
style:height={dateGroup.geometry.containerHeight + 'px'}
style:width={dateGroup.geometry.containerWidth + 'px'}
style:height={geometry.containerHeight + 'px'}
style:width={geometry.containerWidth + 'px'}
>
{#each dateGroup.assets as asset, index (asset.id)}
{@const box = dateGroup.geometry.boxes[index]}
{#each dateGroup.assets as asset, i (asset.id)}
<!-- getting these together here in this order is very cache-efficient -->
{@const top = geometry.getTop(i)}
{@const left = geometry.getLeft(i)}
{@const width = geometry.getWidth(i)}
{@const height = geometry.getHeight(i)}
<!-- update ASSET_GRID_PADDING-->
<div
use:intersectionObserver={{
@@ -190,10 +195,10 @@
}}
data-asset-id={asset.id}
class="absolute"
style:width={box.width + 'px'}
style:height={box.height + 'px'}
style:top={box.top + 'px'}
style:left={box.left + 'px'}
style:top={top + 'px'}
style:left={left + 'px'}
style:width={width + 'px'}
style:height={height + 'px'}
>
<Thumbnail
{dateGroup}
@@ -203,7 +208,7 @@
bottom: renderThumbsAtBottomMargin,
top: renderThumbsAtTopMargin,
}}
retrieveElement={$assetStore.pendingScrollAssetId === asset.id}
retrieveElement={assetStore.pendingScrollAssetId === asset.id}
onRetrieveElement={(element) => onRetrieveElement(dateGroup, asset, element)}
showStackedIcon={withStacked}
{showArchiveIcon}
@@ -212,11 +217,11 @@
onClick={(asset) => onClick(dateGroup.assets, dateGroup.groupTitle, asset)}
onSelect={(asset) => assetSelectHandler(asset, dateGroup.assets, dateGroup.groupTitle)}
onMouseEvent={() => assetMouseEventHandler(dateGroup.groupTitle, asset)}
selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
selected={assetInteraction.selectedAssets.has(asset) || assetStore.albumAssets.has(asset.id)}
selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
disabled={$assetStore.albumAssets.has(asset.id)}
thumbnailWidth={box.width}
thumbnailHeight={box.height}
disabled={assetStore.albumAssets.has(asset.id)}
thumbnailWidth={width}
thumbnailHeight={height}
/>
</div>
{/each}

View File

@@ -4,7 +4,7 @@
import type { Action } from '$lib/components/asset-viewer/actions/action';
import { AppRoute, AssetAction } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets.store';
import { AssetBucket, AssetStore, type BucketListener, type ViewportXY } from '$lib/stores/assets-store.svelte';
import { locale, showDeleteModal } from '$lib/stores/preferences.store';
import { isSearchEnabled } from '$lib/stores/search.store';
import { featureFlags } from '$lib/stores/server-config.store';
@@ -117,7 +117,6 @@
const isViewportOrigin = () => {
return viewport.height === 0 && viewport.width === 0;
};
const isEqual = (a: ViewportXY, b: ViewportXY) => {
return a.height == b.height && a.width == b.width && a.x === b.x && a.y === b.y;
};
@@ -130,7 +129,7 @@
}
if ($gridScrollTarget?.at) {
void $assetStore.scheduleScrollToAssetId($gridScrollTarget, () => {
void assetStore.scheduleScrollToAssetId($gridScrollTarget, () => {
element?.scrollTo({ top: 0 });
showSkeleton = false;
});
@@ -166,7 +165,7 @@
if (assetGridUpdate) {
setTimeout(() => {
void $assetStore.updateViewport(safeViewport, true);
void assetStore.updateViewport(safeViewport, true);
const asset = $page.url.searchParams.get('at');
if (asset) {
$gridScrollTarget = { at: asset };
@@ -194,31 +193,10 @@
return () => void 0;
};
const _updateLastIntersectedBucketDate = () => {
let elem = document.elementFromPoint(safeViewport.x + 1, safeViewport.y + 1);
while (elem != null) {
if (elem.id === 'bucket') {
break;
}
elem = elem.parentElement;
}
if (elem) {
lastIntersectedBucketDate = (elem as HTMLElement).dataset.bucketDate;
}
};
const updateLastIntersectedBucketDate = throttle(_updateLastIntersectedBucketDate, 16, {
leading: false,
trailing: true,
});
const scrollTolastIntersectedBucket = (adjustedBucket: AssetBucket, delta: number) => {
if (!lastIntersectedBucketDate) {
_updateLastIntersectedBucketDate();
}
if (lastIntersectedBucketDate) {
const currentIndex = $assetStore.buckets.findIndex((b) => b.bucketDate === lastIntersectedBucketDate);
const deltaIndex = $assetStore.buckets.indexOf(adjustedBucket);
const currentIndex = assetStore.buckets.findIndex((b) => b.bucketDate === lastIntersectedBucketDate);
const deltaIndex = assetStore.buckets.indexOf(adjustedBucket);
if (deltaIndex < currentIndex) {
element?.scrollBy(0, delta);
@@ -235,20 +213,23 @@
};
onMount(() => {
void $assetStore
void assetStore
.init({ bucketListener })
.then(() => ($assetStore.connect(), $assetStore.updateViewport(safeViewport)));
.then(() => (assetStore.connect(), assetStore.updateViewport(safeViewport)));
if (!enableRouting) {
showSkeleton = false;
}
const dispose = hmrSupport();
return () => {
$assetStore.disconnect();
$assetStore.destroy();
assetStore.disconnect();
assetStore.destroy();
dispose();
};
});
const _updateViewport = () => void assetStore.updateViewport(safeViewport);
const updateViewport = throttle(_updateViewport, 16);
function getOffset(bucketDate: string) {
let offset = 0;
for (let a = 0; a < assetStore.buckets.length; a++) {
@@ -259,12 +240,10 @@
}
return offset;
}
const _updateViewport = () => void $assetStore.updateViewport(safeViewport);
const updateViewport = throttle(_updateViewport, 16);
const getMaxScrollPercent = () =>
($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) /
($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight);
(assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) /
(assetStore.timelineHeight + bottomSectionHeight + topSectionHeight);
const getMaxScroll = () => {
if (!element || !timelineElement) {
@@ -292,7 +271,7 @@
scrollPercent: number,
bucketScrollPercent: number,
) => {
if (!bucketDate || $assetStore.timelineHeight < safeViewport.height * 2) {
if (!bucketDate || assetStore.timelineHeight < safeViewport.height * 2) {
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
const maxScroll = getMaxScroll();
@@ -318,7 +297,7 @@
_scrollPercent: number,
bucketScrollPercent: number,
) => {
if (!bucketDate || $assetStore.timelineHeight < safeViewport.height * 2) {
if (!bucketDate || assetStore.timelineHeight < safeViewport.height * 2) {
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
return;
}
@@ -328,10 +307,7 @@
}
if (bucket && !bucket.measured) {
preMeasure.push(bucket);
if (!bucket.loaded) {
await assetStore.loadBucket(bucket.bucketDate);
}
// Wait here, and collect the deltas that are above offset, which affect offset position
await assetStore.loadBucket(bucketDate, { preventCancel: true, pending: true });
await bucket.measuredPromise;
scrollToBucketAndOffset(bucket, bucketScrollPercent);
}
@@ -354,7 +330,7 @@
return;
}
if ($assetStore.timelineHeight < safeViewport.height * 2) {
if (assetStore.timelineHeight < safeViewport.height * 2) {
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
const maxScroll = getMaxScroll();
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
@@ -424,19 +400,15 @@
preMeasure.push(bucket);
}
showSkeleton = false;
$assetStore.clearPendingScroll();
assetStore.clearPendingScroll();
// set intersecting true manually here, to reduce flicker that happens when
// clearing pending scroll, but the intersection observer hadn't yet had time to run
$assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
};
const trashOrDelete = async (force: boolean = false) => {
isShowDeleteConfirmation = false;
await deleteAssets(
!(isTrashEnabled && !force),
(assetIds) => $assetStore.removeAssets(assetIds),
idsSelectedAssets,
);
await deleteAssets(!(isTrashEnabled && !force), (assetIds) => assetStore.removeAssets(assetIds), idsSelectedAssets);
assetInteraction.clearMultiselect();
};
@@ -461,7 +433,7 @@
const onStackAssets = async () => {
const ids = await stackAssets(assetInteraction.selectedAssetsArray);
if (ids) {
$assetStore.removeAssets(ids);
assetStore.removeAssets(ids);
onEscape();
}
};
@@ -469,7 +441,7 @@
const toggleArchive = async () => {
const ids = await archiveAssets(assetInteraction.selectedAssetsArray, !assetInteraction.isAllArchived);
if (ids) {
$assetStore.removeAssets(ids);
assetStore.removeAssets(ids);
deselectAllAssets();
}
};
@@ -481,33 +453,33 @@
};
const handleSelectAsset = (asset: AssetResponseDto) => {
if (!$assetStore.albumAssets.has(asset.id)) {
if (!assetStore.albumAssets.has(asset.id)) {
assetInteraction.selectAsset(asset);
}
};
function handleIntersect(bucket: AssetBucket) {
updateLastIntersectedBucketDate();
// updateLastIntersectedBucketDate();
const task = () => {
$assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
void $assetStore.loadBucket(bucket.bucketDate);
assetStore.updateBucket(bucket.bucketDate, { intersecting: true });
void assetStore.loadBucket(bucket.bucketDate);
};
$assetStore.taskManager.intersectedBucket(componentId, bucket, task);
assetStore.taskManager.intersectedBucket(componentId, bucket, task);
}
function handleSeparate(bucket: AssetBucket) {
const task = () => {
$assetStore.updateBucket(bucket.bucketDate, { intersecting: false });
assetStore.updateBucket(bucket.bucketDate, { intersecting: false });
bucket.cancel();
};
$assetStore.taskManager.separatedBucket(componentId, bucket, task);
assetStore.taskManager.separatedBucket(componentId, bucket, task);
}
const handlePrevious = async () => {
const previousAsset = await $assetStore.getPreviousAsset($viewingAsset);
const previousAsset = await assetStore.getPreviousAsset($viewingAsset);
if (previousAsset) {
const preloadAsset = await $assetStore.getPreviousAsset(previousAsset);
const preloadAsset = await assetStore.getPreviousAsset(previousAsset);
assetViewingStore.setAsset(previousAsset, preloadAsset ? [preloadAsset] : []);
await navigate({ targetRoute: 'current', assetId: previousAsset.id });
}
@@ -516,9 +488,10 @@
};
const handleNext = async () => {
const nextAsset = await $assetStore.getNextAsset($viewingAsset);
const nextAsset = await assetStore.getNextAsset($viewingAsset);
if (nextAsset) {
const preloadAsset = await $assetStore.getNextAsset(nextAsset);
const preloadAsset = await assetStore.getNextAsset(nextAsset);
assetViewingStore.setAsset(nextAsset, preloadAsset ? [preloadAsset] : []);
await navigate({ targetRoute: 'current', assetId: nextAsset.id });
}
@@ -527,10 +500,10 @@
};
const handleRandom = async () => {
const randomAsset = await $assetStore.getRandomAsset();
const randomAsset = await assetStore.getRandomAsset();
if (randomAsset) {
const preloadAsset = await $assetStore.getNextAsset(randomAsset);
const preloadAsset = await assetStore.getNextAsset(randomAsset);
assetViewingStore.setAsset(randomAsset, preloadAsset ? [preloadAsset] : []);
await navigate({ targetRoute: 'current', assetId: randomAsset.id });
}
@@ -664,8 +637,8 @@
assetInteraction.clearAssetSelectionCandidates();
if (assetInteraction.assetSelectionStart && rangeSelection) {
let startBucketIndex = $assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id);
let endBucketIndex = $assetStore.getBucketIndexByAssetId(asset.id);
let startBucketIndex = assetStore.getBucketIndexByAssetId(assetInteraction.assetSelectionStart.id);
let endBucketIndex = assetStore.getBucketIndexByAssetId(asset.id);
if (startBucketIndex === null || endBucketIndex === null) {
return;
@@ -677,8 +650,8 @@
// Select/deselect assets in all intermediate buckets
for (let bucketIndex = startBucketIndex + 1; bucketIndex < endBucketIndex; bucketIndex++) {
const bucket = $assetStore.buckets[bucketIndex];
await $assetStore.loadBucket(bucket.bucketDate);
const bucket = assetStore.buckets[bucketIndex];
await assetStore.loadBucket(bucket.bucketDate);
for (const asset of bucket.assets) {
if (deselect) {
assetInteraction.removeAssetFromMultiselectGroup(asset);
@@ -690,7 +663,7 @@
// Update date group selection
for (let bucketIndex = startBucketIndex; bucketIndex <= endBucketIndex; bucketIndex++) {
const bucket = $assetStore.buckets[bucketIndex];
const bucket = assetStore.buckets[bucketIndex];
// Split bucket into date groups and check each group
const assetsGroupByDate = splitBucketIntoDateGroups(bucket, $locale);
@@ -718,14 +691,14 @@
return;
}
let start = $assetStore.assets.findIndex((a) => a.id === startAsset.id);
let end = $assetStore.assets.findIndex((a) => a.id === endAsset.id);
let start = assetStore.assets.findIndex((a) => a.id === startAsset.id);
let end = assetStore.assets.findIndex((a) => a.id === endAsset.id);
if (start > end) {
[start, end] = [end, start];
}
assetInteraction.setAssetSelectionCandidates($assetStore.assets.slice(start, end + 1));
assetInteraction.setAssetSelectionCandidates(assetStore.assets.slice(start, end + 1));
};
const onSelectStart = (e: Event) => {
@@ -737,7 +710,7 @@
assetStore.taskManager.removeAllTasksForComponent(componentId);
});
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
let isEmpty = $derived($assetStore.initialized && $assetStore.buckets.length === 0);
let isEmpty = $derived(assetStore.initialized && assetStore.buckets.length === 0);
let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
$effect(() => {
@@ -773,7 +746,7 @@
{ shortcut: { key: 'Escape' }, onShortcut: onEscape },
{ shortcut: { key: '?', shift: true }, onShortcut: () => (showShortcuts = !showShortcuts) },
{ shortcut: { key: '/' }, onShortcut: () => goto(AppRoute.EXPLORE) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets($assetStore, assetInteraction) },
{ shortcut: { key: 'A', ctrl: true }, onShortcut: () => selectAllAssets(assetStore, assetInteraction) },
{ shortcut: { key: 'PageDown' }, preventDefault: false, onShortcut: focusElement },
{ shortcut: { key: 'PageUp' }, preventDefault: false, onShortcut: focusElement },
];
@@ -824,7 +797,7 @@
{#if showShortcuts}
<ShowShortcuts onClose={() => (showShortcuts = !showShortcuts)} />
{/if}
{#if $assetStore.buckets.length > 0}
{#if assetStore.buckets.length > 0}
<Scrubber
invisible={showSkeleton}
{assetStore}
@@ -864,21 +837,33 @@
bind:this={timelineElement}
id="virtual-timeline"
class:invisible={showSkeleton}
style:height={$assetStore.timelineHeight + 'px'}
style:height={assetStore.timelineHeight + 'px'}
>
{#each $assetStore.buckets as bucket (bucket.viewId)}
{#each assetStore.buckets as bucket (bucket.viewId)}
{@const isPremeasure = preMeasure.includes(bucket)}
{@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure}
{@const display = bucket.intersecting || bucket === assetStore.pendingScrollBucket || isPremeasure}
<div
class="bucket"
use:intersectionObserver={{
key: bucket.viewId,
onIntersect: () => handleIntersect(bucket),
onSeparate: () => handleSeparate(bucket),
top: BUCKET_INTERSECTION_ROOT_TOP,
bottom: BUCKET_INTERSECTION_ROOT_BOTTOM,
root: element,
}}
style:overflow={bucket.measured ? 'visible' : 'clip'}
use:intersectionObserver={[
{
key: bucket.viewId,
onIntersect: () => handleIntersect(bucket),
onSeparate: () => handleSeparate(bucket),
top: BUCKET_INTERSECTION_ROOT_TOP,
bottom: BUCKET_INTERSECTION_ROOT_BOTTOM,
root: element,
},
{
key: bucket.viewId + '.bucketintersection',
onIntersect: () => (lastIntersectedBucketDate = bucket.bucketDate),
top: '0px',
bottom: '-' + Math.max(0, safeViewport.height - 1) + 'px',
left: '0px',
right: '0px',
},
]}
data-bucket-display={bucket.intersecting}
data-bucket-date={bucket.bucketDate}
style:height={bucket.bucketHeight + 'px'}
@@ -949,6 +934,5 @@
.bucket {
contain: layout size;
transition: height 0.2s ease-out;
}
</style>

View File

@@ -18,7 +18,7 @@
<script lang="ts">
import { resizeObserver } from '$lib/actions/resize-observer';
import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets.store';
import type { AssetBucket, AssetStore, BucketListener } from '$lib/stores/assets-store.svelte';
interface Props {
assetStore: AssetStore;
@@ -43,11 +43,11 @@
if (!heightPending) {
const height = element.getBoundingClientRect().height;
if (height !== 0) {
$assetStore.updateBucket(bucket.bucketDate, { height, measured: true });
assetStore.updateBucket(bucket.bucketDate, { height, measured: true });
}
onMeasured();
$assetStore.removeListener(listener);
assetStore.removeListener(listener);
const t2 = Date.now();
addMeasure((t2 - t1) / bucket.bucketCount);
@@ -69,7 +69,7 @@
<section id="measure-asset-group-by-date" class="flex flex-wrap gap-x-12" use:measure>
{#each bucket.dateGroups as dateGroup (dateGroup.date)}
<div id="date-group" data-date-group={dateGroup.date}>
<div use:resizeObserver={({ height }) => $assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
<div use:resizeObserver={({ height }) => assetStore.updateBucketDateGroup(bucket, dateGroup, { height })}>
<div
class="flex z-[100] sticky top-[-1px] pt-7 pb-5 h-6 place-items-center text-xs font-medium text-immich-fg bg-immich-bg dark:bg-immich-dark-bg dark:text-immich-dark-fg md:text-sm"
style:width={dateGroup.geometry.containerWidth + 'px'}
@@ -81,8 +81,8 @@
<div
class="relative overflow-clip"
style:height={dateGroup.geometry.containerHeight + 'px'}
style:width={dateGroup.geometry.containerWidth + 'px'}
style:height={dateGroup.geometry!.containerHeight + 'px'}
style:width={dateGroup.geometry!.containerWidth + 'px'}
style:visibility="hidden"
></div>
</div>

View File

@@ -17,7 +17,7 @@
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';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { t } from 'svelte-i18n';
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';

View File

@@ -5,7 +5,7 @@
import CircleIconButton from '../elements/buttons/circle-icon-button.svelte';
import { fly } from 'svelte/transition';
import { mdiClose } from '@mdi/js';
import { isSelectingAllAssets } from '$lib/stores/assets.store';
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
import { t } from 'svelte-i18n';
interface Props {

View File

@@ -5,7 +5,7 @@
import Thumbnail from '$lib/components/assets/thumbnail/thumbnail.svelte';
import { AppRoute, AssetAction } from '$lib/constants';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import type { Viewport } from '$lib/stores/assets.store';
import type { Viewport } from '$lib/stores/assets-store.svelte';
import { showDeleteModal } from '$lib/stores/preferences.store';
import { deleteAssets } from '$lib/utils/actions';
import { archiveAssets, cancelMultiselect, getAssetRatio } from '$lib/utils/asset-utils';

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import type { AssetStore, AssetBucket, BucketListener } from '$lib/stores/assets.store';
import type { AssetStore, AssetBucket, BucketListener } from '$lib/stores/assets-store.svelte';
import { DateTime } from 'luxon';
import { fromLocalDateTime, type ScrubberListener } from '$lib/utils/timeline-util';
import { clamp } from 'lodash-es';
@@ -92,14 +92,14 @@
scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent);
});
let timelineFullHeight = $derived($assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
let timelineFullHeight = $derived(assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
const listener: BucketListener = (event) => {
const { type } = event;
if (type === 'viewport') {
segments = calculateSegments($assetStore.buckets);
segments = calculateSegments(assetStore.buckets);
scrollY = toScrollFromBucketPercentage(scrubBucket, scrubBucketPercent, scrubOverallPercent);
}
};
@@ -128,7 +128,7 @@
for (const [i, bucket] of buckets.entries()) {
const scrollBarPercentage =
bucket.bucketHeight / ($assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
bucket.bucketHeight / (assetStore.timelineHeight + timelineTopOffset + timelineBottomOffset);
const segment = {
count: bucket.assets.length,