mirror of
https://github.com/immich-app/immich.git
synced 2025-12-19 09:13:14 +03:00
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:
@@ -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';
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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';
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user