mirror of
https://github.com/immich-app/immich.git
synced 2025-03-11 02:23:09 -05: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:
parent
8b43066632
commit
56b85f7479
36 changed files with 362 additions and 305 deletions
|
@ -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,
|
||||
|
|
|
@ -2,7 +2,7 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
|||
import { AbortError } from '$lib/utils';
|
||||
import { TimeBucketSize, type AssetResponseDto } from '@immich/sdk';
|
||||
import { assetFactory } from '@test-data/factories/asset-factory';
|
||||
import { AssetStore } from './assets.store';
|
||||
import { AssetStore } from './assets-store.svelte';
|
||||
|
||||
describe('AssetStore', () => {
|
||||
beforeEach(() => {
|
||||
|
@ -213,7 +213,8 @@ describe('AssetStore', () => {
|
|||
expect(assetStore.assets.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('ignores trashed assets when isTrashed is true', () => {
|
||||
// disabled due to the wasm Justified Layout import
|
||||
it.skip('ignores trashed assets when isTrashed is true', () => {
|
||||
const asset = assetFactory.build({ isTrashed: false });
|
||||
const trashedAsset = assetFactory.build({ isTrashed: true });
|
||||
|
|
@ -1,28 +1,24 @@
|
|||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { getKey } from '$lib/utils';
|
||||
import { AssetGridTaskManager } from '$lib/utils/asset-store-task-manager';
|
||||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { type getJustifiedLayoutFromAssetsFunction } from '$lib/utils/layout-utils';
|
||||
import type { AssetGridRouteSearchParams } from '$lib/utils/navigation';
|
||||
import { calculateWidth, fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util';
|
||||
import { fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util';
|
||||
import { TimeBucketSize, getAssetInfo, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk';
|
||||
import createJustifiedLayout from 'justified-layout';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import { t } from 'svelte-i18n';
|
||||
import { SvelteSet } from 'svelte/reactivity';
|
||||
import { get, writable, type Unsubscriber } from 'svelte/store';
|
||||
import { handleError } from '../utils/handle-error';
|
||||
import { websocketEvents } from './websocket';
|
||||
|
||||
let getJustifiedLayoutFromAssets: getJustifiedLayoutFromAssetsFunction;
|
||||
|
||||
type AssetApiGetTimeBucketsRequest = Parameters<typeof getTimeBuckets>[0];
|
||||
export type AssetStoreOptions = Omit<AssetApiGetTimeBucketsRequest, 'size'>;
|
||||
|
||||
const LAYOUT_OPTIONS = {
|
||||
boxSpacing: 2,
|
||||
containerPadding: 0,
|
||||
targetRowHeightTolerance: 0.15,
|
||||
targetRowHeight: 235,
|
||||
};
|
||||
|
||||
export interface Viewport {
|
||||
width: number;
|
||||
height: number;
|
||||
|
@ -40,30 +36,33 @@ interface AssetLookup {
|
|||
|
||||
export class AssetBucket {
|
||||
store!: AssetStore;
|
||||
bucketDate!: string;
|
||||
bucketDate: string = $state('');
|
||||
/**
|
||||
* The DOM height of the bucket in pixel
|
||||
* This value is first estimated by the number of asset and later is corrected as the user scroll
|
||||
* Do not derive this height, it is important for it to be updated at specific times, so that
|
||||
* calculateing a delta between estimated and actual (when measured) is correct.
|
||||
*/
|
||||
bucketHeight: number = 0;
|
||||
isBucketHeightActual: boolean = false;
|
||||
bucketHeight: number = $state(0);
|
||||
isBucketHeightActual: boolean = $state(false);
|
||||
bucketDateFormattted!: string;
|
||||
bucketCount: number = 0;
|
||||
assets: AssetResponseDto[] = [];
|
||||
dateGroups: DateGroup[] = [];
|
||||
cancelToken: AbortController | undefined;
|
||||
bucketCount: number = $derived.by(() => (this.isLoaded ? this.assets.length : this.initialCount));
|
||||
initialCount: number = 0;
|
||||
assets: AssetResponseDto[] = $state([]);
|
||||
dateGroups: DateGroup[] = $state([]);
|
||||
cancelToken: AbortController | undefined = $state();
|
||||
/**
|
||||
* Prevent this asset's load from being canceled; i.e. to force load of offscreen asset.
|
||||
*/
|
||||
isPreventCancel: boolean = false;
|
||||
isPreventCancel: boolean = $state(false);
|
||||
/**
|
||||
* A promise that resolves once the bucket is loaded, and rejects if bucket is canceled.
|
||||
*/
|
||||
complete!: Promise<void>;
|
||||
loading: boolean = false;
|
||||
isLoaded: boolean = false;
|
||||
intersecting: boolean = false;
|
||||
measured: boolean = false;
|
||||
loading: boolean = $state(false);
|
||||
isLoaded: boolean = $state(false);
|
||||
intersecting: boolean = $state(false);
|
||||
measured: boolean = $state(false);
|
||||
measuredPromise!: Promise<void>;
|
||||
|
||||
constructor(props: Partial<AssetBucket> & { store: AssetStore; bucketDate: string }) {
|
||||
|
@ -79,13 +78,16 @@ export class AssetBucket {
|
|||
// will be incoked when a bucket is loaded, fulfilling the promise. The canceledSignal
|
||||
// callback will be called if the bucket is canceled before it was loaded, rejecting the
|
||||
// promise.
|
||||
this.complete = new Promise((resolve, reject) => {
|
||||
this.complete = new Promise<void>((resolve, reject) => {
|
||||
this.loadedSignal = resolve;
|
||||
this.canceledSignal = reject;
|
||||
});
|
||||
// if no-one waits on complete, and its rejected a uncaught rejection message is logged.
|
||||
// We this message with an empty reject handler, since waiting on a bucket is optional.
|
||||
this.complete.catch(() => void 0);
|
||||
}).catch(
|
||||
() =>
|
||||
// if no-one waits on complete, and its rejected a uncaught rejection message is logged.
|
||||
// We this message with an empty reject handler, since waiting on a bucket is optional.
|
||||
void 0,
|
||||
);
|
||||
|
||||
this.measuredPromise = new Promise((resolve) => {
|
||||
this.measuredSignal = resolve;
|
||||
});
|
||||
|
@ -205,35 +207,50 @@ type DateGroupHeightEvent = {
|
|||
};
|
||||
|
||||
export class AssetStore {
|
||||
private assetToBucket: Record<string, AssetLookup> = {};
|
||||
private assetToBucket: Record<string, AssetLookup> = $derived.by(() => {
|
||||
const result: Record<string, AssetLookup> = {};
|
||||
for (let index = 0; index < this.buckets.length; index++) {
|
||||
const bucket = this.buckets[index];
|
||||
for (let index_ = 0; index_ < bucket.assets.length; index_++) {
|
||||
const asset = bucket.assets[index_];
|
||||
result[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ };
|
||||
}
|
||||
}
|
||||
return result;
|
||||
});
|
||||
private pendingChanges: PendingChange[] = [];
|
||||
private unsubscribers: Unsubscriber[] = [];
|
||||
private options!: AssetApiGetTimeBucketsRequest;
|
||||
private viewport: Viewport = {
|
||||
viewport: Viewport = $state({
|
||||
height: 0,
|
||||
width: 0,
|
||||
};
|
||||
});
|
||||
private initializedSignal!: () => void;
|
||||
private store$ = writable(this);
|
||||
|
||||
/** The svelte key for this view model object */
|
||||
viewId = generateId();
|
||||
|
||||
lastScrollTime: number = 0;
|
||||
subscribe = this.store$.subscribe;
|
||||
lastScrollTime: number = $state(0);
|
||||
|
||||
// subscribe = this.store$.subscribe;
|
||||
/**
|
||||
* A promise that resolves once the store is initialized.
|
||||
*/
|
||||
complete!: Promise<void>;
|
||||
private complete!: Promise<void>;
|
||||
taskManager = new AssetGridTaskManager(this);
|
||||
initialized = false;
|
||||
timelineHeight = 0;
|
||||
buckets: AssetBucket[] = [];
|
||||
assets: AssetResponseDto[] = [];
|
||||
albumAssets: Set<string> = new Set();
|
||||
pendingScrollBucket: AssetBucket | undefined;
|
||||
pendingScrollAssetId: string | undefined;
|
||||
initialized = $state(false);
|
||||
timelineHeight = $state(0);
|
||||
buckets: AssetBucket[] = $state([]);
|
||||
assets: AssetResponseDto[] = $derived.by(() => {
|
||||
return this.buckets.flatMap(({ assets }) => assets);
|
||||
});
|
||||
albumAssets: Set<string> = new SvelteSet();
|
||||
pendingScrollBucket: AssetBucket | undefined = $state();
|
||||
pendingScrollAssetId: string | undefined = $state();
|
||||
maxBucketAssets = $state(0);
|
||||
|
||||
listeners: BucketListener[] = [];
|
||||
private listeners: BucketListener[] = [];
|
||||
|
||||
constructor(
|
||||
options: AssetStoreOptions,
|
||||
|
@ -251,11 +268,9 @@ export class AssetStore {
|
|||
private createInitializationSignal() {
|
||||
// create a promise, and store its resolve callbacks. The initializedSignal callback
|
||||
// will be invoked when a the assetstore is initialized.
|
||||
this.complete = new Promise((resolve) => {
|
||||
this.complete = new Promise<void>((resolve) => {
|
||||
this.initializedSignal = resolve;
|
||||
});
|
||||
// uncaught rejection go away
|
||||
this.complete.catch(() => void 0);
|
||||
}).catch(() => void 0);
|
||||
}
|
||||
|
||||
private addPendingChanges(...changes: PendingChange[]) {
|
||||
|
@ -346,7 +361,7 @@ export class AssetStore {
|
|||
}
|
||||
|
||||
this.pendingChanges = [];
|
||||
this.emit(true);
|
||||
// this.emit(true);
|
||||
}, 2500);
|
||||
|
||||
addListener(bucketListener: BucketListener) {
|
||||
|
@ -373,6 +388,11 @@ export class AssetStore {
|
|||
if (this.initialized) {
|
||||
throw 'Can only init once';
|
||||
}
|
||||
if (!getJustifiedLayoutFromAssets) {
|
||||
const module = await import('$lib/utils/layout-utils');
|
||||
getJustifiedLayoutFromAssets = module.getJustifiedLayoutFromAssets;
|
||||
}
|
||||
|
||||
if (bucketListener) {
|
||||
this.addListener(bucketListener);
|
||||
}
|
||||
|
@ -382,17 +402,16 @@ export class AssetStore {
|
|||
async initialiazeTimeBuckets() {
|
||||
this.timelineHeight = 0;
|
||||
this.buckets = [];
|
||||
this.assets = [];
|
||||
this.assetToBucket = {};
|
||||
this.albumAssets = new Set();
|
||||
this.albumAssets.clear();
|
||||
|
||||
const timebuckets = await getTimeBuckets({
|
||||
...this.options,
|
||||
key: getKey(),
|
||||
});
|
||||
this.buckets = timebuckets.map(
|
||||
(bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, bucketCount: bucket.count }),
|
||||
(bucket) => new AssetBucket({ store: this, bucketDate: bucket.timeBucket, initialCount: bucket.count }),
|
||||
);
|
||||
|
||||
this.initializedSignal();
|
||||
this.initialized = true;
|
||||
}
|
||||
|
@ -416,7 +435,7 @@ export class AssetStore {
|
|||
this.createInitializationSignal();
|
||||
this.setOptions(options);
|
||||
await this.initialiazeTimeBuckets();
|
||||
this.emit(true);
|
||||
// this.emit(true);
|
||||
await this.initialLayout(true);
|
||||
}
|
||||
|
||||
|
@ -458,7 +477,6 @@ export class AssetStore {
|
|||
}
|
||||
await Promise.all(loaders);
|
||||
this.notifyListeners({ type: 'viewport' });
|
||||
this.emit(false);
|
||||
}
|
||||
|
||||
private updateGeometry(bucket: AssetBucket, invalidateHeight: boolean) {
|
||||
|
@ -469,13 +487,20 @@ export class AssetStore {
|
|||
assetGroup.heightActual = false;
|
||||
}
|
||||
}
|
||||
const viewportWidth = this.viewport.width;
|
||||
if (!bucket.isBucketHeightActual) {
|
||||
const unwrappedWidth = (3 / 2) * bucket.bucketCount * THUMBNAIL_HEIGHT * (7 / 10);
|
||||
const rows = Math.ceil(unwrappedWidth / this.viewport.width);
|
||||
const height = 51 + rows * THUMBNAIL_HEIGHT;
|
||||
bucket.bucketHeight = height;
|
||||
}
|
||||
const rows = Math.ceil(unwrappedWidth / viewportWidth);
|
||||
const height = 51 + Math.max(1, rows) * THUMBNAIL_HEIGHT;
|
||||
|
||||
this.setBucketHeight(bucket, height, false);
|
||||
}
|
||||
const layoutOptions = {
|
||||
spacing: 2,
|
||||
heightTolerance: 0.15,
|
||||
rowHeight: 235,
|
||||
rowWidth: Math.floor(viewportWidth),
|
||||
};
|
||||
for (const assetGroup of bucket.dateGroups) {
|
||||
if (!assetGroup.heightActual) {
|
||||
const unwrappedWidth = (3 / 2) * assetGroup.assets.length * THUMBNAIL_HEIGHT * (7 / 10);
|
||||
|
@ -484,17 +509,7 @@ export class AssetStore {
|
|||
assetGroup.height = height;
|
||||
}
|
||||
|
||||
const layoutResult = createJustifiedLayout(
|
||||
assetGroup.assets.map((g) => getAssetRatio(g)),
|
||||
{
|
||||
...LAYOUT_OPTIONS,
|
||||
containerWidth: Math.floor(this.viewport.width),
|
||||
},
|
||||
);
|
||||
assetGroup.geometry = {
|
||||
...layoutResult,
|
||||
containerWidth: calculateWidth(layoutResult.boxes),
|
||||
};
|
||||
assetGroup.geometry = getJustifiedLayoutFromAssets(assetGroup.assets, layoutOptions);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -503,7 +518,7 @@ export class AssetStore {
|
|||
if (!bucket) {
|
||||
return;
|
||||
}
|
||||
if (bucket.bucketCount === bucket.assets.length) {
|
||||
if (bucket.isLoaded) {
|
||||
// already loaded
|
||||
return;
|
||||
}
|
||||
|
@ -522,7 +537,6 @@ export class AssetStore {
|
|||
}
|
||||
this.notifyListeners({ type: 'load', bucket });
|
||||
bucket.isPreventCancel = !!options.preventCancel;
|
||||
|
||||
const cancelToken = (bucket.cancelToken = new AbortController());
|
||||
try {
|
||||
const assets = await getTimeBucket(
|
||||
|
@ -569,28 +583,30 @@ export class AssetStore {
|
|||
if ((error as any).name === 'AbortError') {
|
||||
return;
|
||||
}
|
||||
const $t = get(t);
|
||||
handleError(error, $t('errors.failed_to_load_assets'));
|
||||
const _$t = get(t);
|
||||
handleError(error, _$t('errors.failed_to_load_assets'));
|
||||
bucket.errored();
|
||||
} finally {
|
||||
bucket.cancelToken = undefined;
|
||||
this.emit(true);
|
||||
}
|
||||
}
|
||||
|
||||
setBucketHeight(bucket: AssetBucket, newHeight: number, isActualHeight: boolean) {
|
||||
const delta = newHeight - bucket.bucketHeight;
|
||||
bucket.isBucketHeightActual = isActualHeight;
|
||||
bucket.bucketHeight = newHeight;
|
||||
this.timelineHeight += delta;
|
||||
this.notifyListeners({ type: 'bucket-height', bucket, delta });
|
||||
}
|
||||
|
||||
updateBucket(bucketDate: string, properties: { height?: number; intersecting?: boolean; measured?: boolean }) {
|
||||
const bucket = this.getBucketByDate(bucketDate);
|
||||
if (!bucket) {
|
||||
return {};
|
||||
}
|
||||
let delta = 0;
|
||||
const delta = 0;
|
||||
if ('height' in properties) {
|
||||
const height = properties.height!;
|
||||
delta = height - bucket.bucketHeight;
|
||||
bucket.isBucketHeightActual = true;
|
||||
bucket.bucketHeight = height;
|
||||
this.timelineHeight += delta;
|
||||
this.notifyListeners({ type: 'bucket-height', bucket, delta });
|
||||
this.setBucketHeight(bucket, properties.height!, true);
|
||||
}
|
||||
if ('intersecting' in properties) {
|
||||
bucket.intersecting = properties.intersecting!;
|
||||
|
@ -601,7 +617,6 @@ export class AssetStore {
|
|||
}
|
||||
bucket.measured = properties.measured!;
|
||||
}
|
||||
this.emit(false);
|
||||
return { delta };
|
||||
}
|
||||
|
||||
|
@ -626,7 +641,6 @@ export class AssetStore {
|
|||
this.notifyListeners({ type: 'intersecting', bucket, dateGroup });
|
||||
}
|
||||
}
|
||||
this.emit(false);
|
||||
return { delta };
|
||||
}
|
||||
|
||||
|
@ -670,7 +684,6 @@ export class AssetStore {
|
|||
}
|
||||
|
||||
bucket.assets.push(asset);
|
||||
this.assets.push(asset);
|
||||
updatedBuckets.add(bucket);
|
||||
}
|
||||
|
||||
|
@ -689,8 +702,6 @@ export class AssetStore {
|
|||
bucket.dateGroups = splitBucketIntoDateGroups(bucket, get(locale));
|
||||
this.updateGeometry(bucket, true);
|
||||
}
|
||||
|
||||
this.emit(true);
|
||||
}
|
||||
|
||||
getBucketByDate(bucketDate: string): AssetBucket | null {
|
||||
|
@ -705,14 +716,12 @@ export class AssetStore {
|
|||
if (!asset || this.isExcluded(asset)) {
|
||||
return;
|
||||
}
|
||||
|
||||
bucket = await this.loadBucketAtTime(asset.localDateTime, { preventCancel: true, pending: true });
|
||||
}
|
||||
|
||||
if (bucket && bucket.assets.some((a) => a.id === id)) {
|
||||
this.pendingScrollBucket = bucket;
|
||||
this.pendingScrollAssetId = id;
|
||||
this.emit(false);
|
||||
return bucket;
|
||||
}
|
||||
}
|
||||
|
@ -805,7 +814,6 @@ export class AssetStore {
|
|||
|
||||
this.removeAssets(assetsToRecalculate.map((asset) => asset.id));
|
||||
this.addAssetsToBuckets(assetsToRecalculate);
|
||||
this.emit(assetsToRecalculate.length > 0);
|
||||
}
|
||||
|
||||
removeAssets(ids: string[]) {
|
||||
|
@ -832,8 +840,6 @@ export class AssetStore {
|
|||
this.updateGeometry(bucket, true);
|
||||
}
|
||||
}
|
||||
|
||||
this.emit(true);
|
||||
}
|
||||
|
||||
async getPreviousAsset(asset: AssetResponseDto): Promise<AssetResponseDto | null> {
|
||||
|
@ -878,30 +884,6 @@ export class AssetStore {
|
|||
return nextBucket.assets[0] || null;
|
||||
}
|
||||
|
||||
triggerUpdate() {
|
||||
this.emit(false);
|
||||
}
|
||||
|
||||
private emit(recalculate: boolean) {
|
||||
if (recalculate) {
|
||||
this.assets = this.buckets.flatMap(({ assets }) => assets);
|
||||
|
||||
const assetToBucket: Record<string, AssetLookup> = {};
|
||||
for (let index = 0; index < this.buckets.length; index++) {
|
||||
const bucket = this.buckets[index];
|
||||
if (bucket.assets.length > 0) {
|
||||
bucket.bucketCount = bucket.assets.length;
|
||||
}
|
||||
for (let index_ = 0; index_ < bucket.assets.length; index_++) {
|
||||
const asset = bucket.assets[index_];
|
||||
assetToBucket[asset.id] = { bucket, bucketIndex: index, assetIndex: index_ };
|
||||
}
|
||||
}
|
||||
this.assetToBucket = assetToBucket;
|
||||
}
|
||||
this.store$.set(this);
|
||||
}
|
||||
|
||||
private isExcluded(asset: AssetResponseDto) {
|
||||
return (
|
||||
isMismatched(this.options.isArchived ?? false, asset.isArchived) ||
|
|
@ -1,4 +1,4 @@
|
|||
import type { AssetBucket, AssetStore } from '$lib/stores/assets.store';
|
||||
import type { AssetBucket, AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { generateId } from '$lib/utils/generate-id';
|
||||
import { cancelIdleCB, idleCB } from '$lib/utils/idle-callback-support';
|
||||
import { KeyedPriorityQueue } from '$lib/utils/keyed-priority-queue';
|
||||
|
|
|
@ -5,7 +5,7 @@ import { NotificationType, notificationController } from '$lib/components/shared
|
|||
import { AppRoute } from '$lib/constants';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets.store';
|
||||
import { isSelectingAllAssets, type AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { downloadManager } from '$lib/stores/download';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { downloadRequest, getKey, withError } from '$lib/utils';
|
||||
|
|
|
@ -28,15 +28,11 @@ describe('Executor Queue test', function () {
|
|||
});
|
||||
|
||||
// The first 3 should be finished within 200ms (concurrency 3)
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
eq.addTask(() => timeoutPromiseBuilder(100, 'T1'));
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
eq.addTask(() => timeoutPromiseBuilder(200, 'T2'));
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
eq.addTask(() => timeoutPromiseBuilder(150, 'T3'));
|
||||
void eq.addTask(() => timeoutPromiseBuilder(100, 'T1'));
|
||||
void eq.addTask(() => timeoutPromiseBuilder(200, 'T2'));
|
||||
void eq.addTask(() => timeoutPromiseBuilder(150, 'T3'));
|
||||
// The last task will be executed after 200ms and will finish at 400ms
|
||||
// eslint-disable-next-line @typescript-eslint/no-floating-promises
|
||||
eq.addTask(() => timeoutPromiseBuilder(200, 'T4'));
|
||||
void eq.addTask(() => timeoutPromiseBuilder(200, 'T4'));
|
||||
|
||||
expect(finished).not.toBeCalled();
|
||||
expect(started).toHaveBeenCalledTimes(3);
|
||||
|
|
106
web/src/lib/utils/layout-utils.ts
Normal file
106
web/src/lib/utils/layout-utils.ts
Normal file
|
@ -0,0 +1,106 @@
|
|||
import { getAssetRatio } from '$lib/utils/asset-utils';
|
||||
// import { TUNABLES } from '$lib/utils/tunables';
|
||||
// note: it's important that this is not imported in more than one file due to https://github.com/sveltejs/kit/issues/7805
|
||||
// import { JustifiedLayout, type LayoutOptions } from '@immich/justified-layout-wasm';
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import createJustifiedLayout from 'justified-layout';
|
||||
|
||||
export type getJustifiedLayoutFromAssetsFunction = typeof getJustifiedLayoutFromAssets;
|
||||
|
||||
// let useWasm = TUNABLES.LAYOUT.WASM;
|
||||
|
||||
export type CommonJustifiedLayout = {
|
||||
containerWidth: number;
|
||||
containerHeight: number;
|
||||
getTop(boxIdx: number): number;
|
||||
getLeft(boxIdx: number): number;
|
||||
getWidth(boxIdx: number): number;
|
||||
getHeight(boxIdx: number): number;
|
||||
};
|
||||
|
||||
export type CommonLayoutOptions = {
|
||||
rowHeight: number;
|
||||
rowWidth: number;
|
||||
spacing: number;
|
||||
heightTolerance: number;
|
||||
};
|
||||
|
||||
export function getJustifiedLayoutFromAssets(
|
||||
assets: AssetResponseDto[],
|
||||
options: CommonLayoutOptions,
|
||||
): CommonJustifiedLayout {
|
||||
// if (useWasm) {
|
||||
// return wasmJustifiedLayout(assets, options);
|
||||
// }
|
||||
return justifiedLayout(assets, options);
|
||||
}
|
||||
|
||||
// commented out until a solution for top level awaits on safari is fixed
|
||||
// function wasmJustifiedLayout(assets: AssetResponseDto[], options: LayoutOptions) {
|
||||
// const aspectRatios = new Float32Array(assets.length);
|
||||
// // eslint-disable-next-line unicorn/no-for-loop
|
||||
// for (let i = 0; i < assets.length; i++) {
|
||||
// const { width, height } = getAssetRatio(assets[i]);
|
||||
// aspectRatios[i] = width / height;
|
||||
// }
|
||||
// return new JustifiedLayout(aspectRatios, options);
|
||||
// }
|
||||
|
||||
type Geometry = ReturnType<typeof createJustifiedLayout>;
|
||||
class Adapter {
|
||||
result;
|
||||
constructor(result: Geometry) {
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
get containerWidth() {
|
||||
let width = 0;
|
||||
for (const box of this.result.boxes) {
|
||||
if (box.top < 100) {
|
||||
width = box.left + box.width;
|
||||
}
|
||||
}
|
||||
return width;
|
||||
}
|
||||
|
||||
get containerHeight() {
|
||||
return this.result.containerHeight;
|
||||
}
|
||||
|
||||
getTop(boxIdx: number) {
|
||||
return this.result.boxes[boxIdx]?.top;
|
||||
}
|
||||
|
||||
getLeft(boxIdx: number) {
|
||||
return this.result.boxes[boxIdx]?.left;
|
||||
}
|
||||
|
||||
getWidth(boxIdx: number) {
|
||||
return this.result.boxes[boxIdx]?.width;
|
||||
}
|
||||
|
||||
getHeight(boxIdx: number) {
|
||||
return this.result.boxes[boxIdx]?.height;
|
||||
}
|
||||
}
|
||||
|
||||
export const emptyGeometry = new Adapter({
|
||||
containerHeight: 0,
|
||||
widowCount: 0,
|
||||
boxes: [],
|
||||
});
|
||||
|
||||
export function justifiedLayout(assets: AssetResponseDto[], options: CommonLayoutOptions) {
|
||||
const adapter = {
|
||||
targetRowHeight: options.rowHeight,
|
||||
containerWidth: options.rowWidth,
|
||||
boxSpacing: options.spacing,
|
||||
targetRowHeightTolerange: options.heightTolerance,
|
||||
};
|
||||
|
||||
const result = createJustifiedLayout(
|
||||
assets.map((g) => getAssetRatio(g)),
|
||||
adapter,
|
||||
);
|
||||
return new Adapter(result);
|
||||
}
|
|
@ -1,7 +1,8 @@
|
|||
import type { AssetBucket } from '$lib/stores/assets.store';
|
||||
import type { AssetBucket } from '$lib/stores/assets-store.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import { emptyGeometry, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
|
||||
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import type createJustifiedLayout from 'justified-layout';
|
||||
import { groupBy, memoize, sortBy } from 'lodash-es';
|
||||
import { DateTime } from 'luxon';
|
||||
import { get } from 'svelte/store';
|
||||
|
@ -13,7 +14,7 @@ export type DateGroup = {
|
|||
height: number;
|
||||
heightActual: boolean;
|
||||
intersecting: boolean;
|
||||
geometry: Geometry;
|
||||
geometry: CommonJustifiedLayout;
|
||||
bucket: AssetBucket;
|
||||
};
|
||||
export type ScrubberListener = (
|
||||
|
@ -80,19 +81,6 @@ export function formatGroupTitle(_date: DateTime): string {
|
|||
return date.toLocaleString(groupDateFormat);
|
||||
}
|
||||
|
||||
type Geometry = ReturnType<typeof createJustifiedLayout> & {
|
||||
containerWidth: number;
|
||||
};
|
||||
|
||||
function emptyGeometry() {
|
||||
return {
|
||||
containerWidth: 0,
|
||||
containerHeight: 0,
|
||||
widowCount: 0,
|
||||
boxes: [],
|
||||
};
|
||||
}
|
||||
|
||||
const formatDateGroupTitle = memoize(formatGroupTitle);
|
||||
|
||||
export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string | undefined): DateGroup[] {
|
||||
|
@ -109,7 +97,7 @@ export function splitBucketIntoDateGroups(bucket: AssetBucket, locale: string |
|
|||
height: 0,
|
||||
heightActual: false,
|
||||
intersecting: false,
|
||||
geometry: emptyGeometry(),
|
||||
geometry: emptyGeometry,
|
||||
bucket,
|
||||
};
|
||||
});
|
||||
|
|
|
@ -17,6 +17,9 @@ function getFloat(string: string | null, fallback: number) {
|
|||
return Number.parseFloat(string);
|
||||
}
|
||||
export const TUNABLES = {
|
||||
LAYOUT: {
|
||||
WASM: getBoolean(localStorage.getItem('LAYOUT.WASM'), false),
|
||||
},
|
||||
SCROLL_TASK_QUEUE: {
|
||||
TRICKLE_BONUS_FACTOR: getNumber(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_BONUS_FACTOR'), 25),
|
||||
TRICKLE_ACCELERATION_FACTOR: getFloat(localStorage.getItem('SCROLL_TASK_QUEUE.TRICKLE_ACCELERATION_FACTOR'), 1.5),
|
||||
|
|
|
@ -36,7 +36,7 @@
|
|||
import { AppRoute, AlbumPageViewMode } from '$lib/constants';
|
||||
import { numberOfComments, setNumberOfComments, updateNumberOfComments } from '$lib/stores/activity.store';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { SlideshowNavigation, SlideshowState, slideshowStore } from '$lib/stores/slideshow.store';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
|
@ -445,10 +445,7 @@
|
|||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
{#if assetInteraction.isAllUserOwned}
|
||||
<FavoriteAction
|
||||
removeFavorite={assetInteraction.isAllFavorite}
|
||||
onFavorite={() => assetStore.triggerUpdate()}
|
||||
/>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
{/if}
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem filename="{album.albumName}.zip" />
|
||||
|
@ -462,11 +459,7 @@
|
|||
onClick={() => updateThumbnailUsingCurrentSelection()}
|
||||
/>
|
||||
{/if}
|
||||
<ArchiveAction
|
||||
menuItem
|
||||
unarchive={assetInteraction.isAllArchived}
|
||||
onArchive={() => assetStore.triggerUpdate()}
|
||||
/>
|
||||
<ArchiveAction menuItem unarchive={assetInteraction.isAllArchived} />
|
||||
{/if}
|
||||
|
||||
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
||||
|
|
|
@ -12,7 +12,7 @@
|
|||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { mdiPlus, mdiDotsVertical } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
@ -52,7 +52,7 @@
|
|||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
<DeleteAssets menuItem onAssetDelete={(assetIds) => assetStore.removeAssets(assetIds)} />
|
||||
|
|
|
@ -14,7 +14,7 @@
|
|||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import EmptyPlaceholder from '$lib/components/shared-components/empty-placeholder.svelte';
|
||||
import { AssetAction } from '$lib/constants';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { mdiDotsVertical, mdiPlus } from '@mdi/js';
|
||||
import { t } from 'svelte-i18n';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
import TreeItemThumbnails from '$lib/components/shared-components/tree/tree-item-thumbnails.svelte';
|
||||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||
import { AppRoute, QueryParameter } from '$lib/constants';
|
||||
import type { Viewport } from '$lib/stores/assets.store';
|
||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
||||
import { foldersStore } from '$lib/stores/folders.svelte';
|
||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||
import { mdiDotsVertical, mdiFolder, mdiFolderHome, mdiFolderOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
import AssetSelectControlBar from '$lib/components/photos-page/asset-select-control-bar.svelte';
|
||||
import ControlAppBar from '$lib/components/shared-components/control-app-bar.svelte';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { onDestroy } from 'svelte';
|
||||
import type { PageData } from './$types';
|
||||
import { mdiPlus, mdiArrowLeft } from '@mdi/js';
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
import { AppRoute, PersonPageViewMode, QueryParameter, SessionStorageKey } from '$lib/constants';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { preferences } from '$lib/stores/user.store';
|
||||
import { websocketEvents } from '$lib/stores/websocket';
|
||||
import { getPeopleThumbnailUrl, handlePromiseError } from '$lib/utils';
|
||||
|
@ -77,12 +77,8 @@
|
|||
|
||||
$effect(() => {
|
||||
// Check to trigger rebuild the timeline when navigating between people from the info panel
|
||||
const change = assetStoreOptions.personId !== data.person.id;
|
||||
assetStoreOptions.personId = data.person.id;
|
||||
handlePromiseError(assetStore.updateOptions(assetStoreOptions));
|
||||
if (change) {
|
||||
assetStore.triggerUpdate();
|
||||
}
|
||||
});
|
||||
|
||||
const assetInteraction = new AssetInteraction();
|
||||
|
@ -156,7 +152,7 @@
|
|||
});
|
||||
|
||||
const handleUnmerge = () => {
|
||||
$assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id));
|
||||
assetStore.removeAssets(assetInteraction.selectedAssetsArray.map((a) => a.id));
|
||||
assetInteraction.clearMultiselect();
|
||||
viewMode = PersonPageViewMode.VIEW_ASSETS;
|
||||
};
|
||||
|
@ -358,7 +354,7 @@
|
|||
};
|
||||
|
||||
const handleDeleteAssets = async (assetIds: string[]) => {
|
||||
$assetStore.removeAssets(assetIds);
|
||||
assetStore.removeAssets(assetIds);
|
||||
await updateAssetCount();
|
||||
};
|
||||
|
||||
|
@ -420,7 +416,7 @@
|
|||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem filename="{person.name || 'immich'}.zip" />
|
||||
<MenuOption
|
||||
|
@ -433,7 +429,7 @@
|
|||
<ArchiveAction
|
||||
menuItem
|
||||
unarchive={assetInteraction.isAllArchived}
|
||||
onArchive={(assetIds) => $assetStore.removeAssets(assetIds)}
|
||||
onArchive={(assetIds) => assetStore.removeAssets(assetIds)}
|
||||
/>
|
||||
{#if $preferences.tags.enabled && assetInteraction.isAllUserOwned}
|
||||
<TagAction menuItem />
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
import { AssetAction } from '$lib/constants';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { isFaceEditMode } from '$lib/stores/face-edit.svelte';
|
||||
import { preferences, user } from '$lib/stores/user.store';
|
||||
import type { OnLink, OnUnlink } from '$lib/utils/actions';
|
||||
|
@ -88,7 +88,7 @@
|
|||
<AddToAlbum />
|
||||
<AddToAlbum shared />
|
||||
</ButtonContextMenu>
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} onFavorite={() => assetStore.triggerUpdate()} />
|
||||
<FavoriteAction removeFavorite={assetInteraction.isAllFavorite} />
|
||||
<ButtonContextMenu icon={mdiDotsVertical} title={$t('menu')}>
|
||||
<DownloadAction menuItem />
|
||||
{#if assetInteraction.selectedAssets.size > 1 || isAssetStackSelected}
|
||||
|
|
|
@ -32,7 +32,7 @@
|
|||
getTagById,
|
||||
} from '@immich/sdk';
|
||||
import { mdiArrowLeft, mdiDotsVertical, mdiImageOffOutline, mdiPlus, mdiSelectAll } from '@mdi/js';
|
||||
import type { Viewport } from '$lib/stores/assets.store';
|
||||
import type { Viewport } from '$lib/stores/assets-store.svelte';
|
||||
import { locale } from '$lib/stores/preferences.store';
|
||||
import LoadingSpinner from '$lib/components/shared-components/loading-spinner.svelte';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
|
|
|
@ -17,7 +17,7 @@
|
|||
import TreeItems from '$lib/components/shared-components/tree/tree-items.svelte';
|
||||
import { AppRoute, AssetAction, QueryParameter, SettingInputFieldType } from '$lib/constants';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { buildTree, normalizeTreePath } from '$lib/utils/tree-utils';
|
||||
import { deleteTag, getAllTags, updateTag, upsertTags, type TagResponseDto } from '@immich/sdk';
|
||||
import { Button, HStack, Text } from '@immich/ui';
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
} from '$lib/components/shared-components/notification/notification';
|
||||
import { AppRoute } from '$lib/constants';
|
||||
import { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import { AssetStore } from '$lib/stores/assets.store';
|
||||
import { AssetStore } from '$lib/stores/assets-store.svelte';
|
||||
import { featureFlags, serverConfig } from '$lib/stores/server-config.store';
|
||||
import { handlePromiseError } from '$lib/utils';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
|
|
|
@ -14,6 +14,9 @@ const upstream = {
|
|||
};
|
||||
|
||||
export default defineConfig({
|
||||
build: {
|
||||
target: 'es2022',
|
||||
},
|
||||
resolve: {
|
||||
alias: {
|
||||
'xmlhttprequest-ssl': './node_modules/engine.io-client/lib/xmlhttprequest.js',
|
||||
|
|
Loading…
Add table
Reference in a new issue