mirror of
https://github.com/immich-app/immich
synced 2025-11-07 17:27:20 +00:00
chore(web): revert wasm new justify layout (#16277)
* Revert "fix(web): justify layout import (#16267) " This reverts commitec58e1065f. * Revert "fix(web): dynamically import wasm module (#16261)" This reverts commit4376fd72b7. * Revert "feat(web): use wasm for justified layout calculation (#15524)" This reverts commit3925445de8. * Revert "fix(web): viewport reactivity, off-screen thumbhashes being rendered (#15435)" This reverts commit52f21fb331.
This commit is contained in:
parent
16266c9f5a
commit
bbcaee82f0
14 changed files with 201 additions and 330 deletions
|
|
@ -39,7 +39,6 @@
|
|||
thumbnailSize?: number | undefined;
|
||||
thumbnailWidth?: number | undefined;
|
||||
thumbnailHeight?: number | undefined;
|
||||
eagerThumbhash?: boolean;
|
||||
selected?: boolean;
|
||||
selectionCandidate?: boolean;
|
||||
disabled?: boolean;
|
||||
|
|
@ -72,7 +71,6 @@
|
|||
thumbnailSize = undefined,
|
||||
thumbnailWidth = undefined,
|
||||
thumbnailHeight = undefined,
|
||||
eagerThumbhash = true,
|
||||
selected = false,
|
||||
selectionCandidate = false,
|
||||
disabled = false,
|
||||
|
|
@ -115,6 +113,7 @@
|
|||
|
||||
let width = $derived(thumbnailSize || thumbnailWidth || 235);
|
||||
let height = $derived(thumbnailSize || thumbnailHeight || 235);
|
||||
let display = $derived(intersecting);
|
||||
|
||||
const onIconClickedHandler = (e?: MouseEvent) => {
|
||||
e?.stopPropagation();
|
||||
|
|
@ -208,11 +207,7 @@
|
|||
? 'bg-gray-300'
|
||||
: 'bg-immich-primary/20 dark:bg-immich-dark-primary/20'}"
|
||||
>
|
||||
<!-- TODO: Rendering thumbhashes for offscreen assets is a ton of overhead.
|
||||
This is here to ensure thumbhashes appear on the first
|
||||
frame instead of a gray box for smaller date groups,
|
||||
where the overhead (while wasteful) does not cause major issues. -->
|
||||
{#if eagerThumbhash && !loaded && asset.thumbhash}
|
||||
{#if !loaded && asset.thumbhash}
|
||||
<canvas
|
||||
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
|
||||
class="absolute object-cover z-10"
|
||||
|
|
@ -222,17 +217,7 @@
|
|||
></canvas>
|
||||
{/if}
|
||||
|
||||
{#if intersecting}
|
||||
{#if !eagerThumbhash && !loaded && asset.thumbhash}
|
||||
<canvas
|
||||
use:thumbhash={{ base64ThumbHash: asset.thumbhash }}
|
||||
class="absolute object-cover z-10"
|
||||
style:width="{width}px"
|
||||
style:height="{height}px"
|
||||
out:fade={{ duration: THUMBHASH_FADE_DURATION }}
|
||||
></canvas>
|
||||
{/if}
|
||||
|
||||
{#if display}
|
||||
<!-- svelte queries for all links on afterNavigate, leading to performance problems in asset-grid which updates
|
||||
the navigation url on scroll. Replace this with button for now. -->
|
||||
<div
|
||||
|
|
|
|||
|
|
@ -38,7 +38,7 @@
|
|||
$: dateGroups = bucket.dateGroups;
|
||||
|
||||
const {
|
||||
DATEGROUP: { INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM, SMALL_GROUP_THRESHOLD },
|
||||
DATEGROUP: { INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM },
|
||||
} = TUNABLES;
|
||||
/* TODO figure out a way to calculate this*/
|
||||
const TITLE_HEIGHT = 51;
|
||||
|
|
@ -97,7 +97,6 @@
|
|||
{#each dateGroups as dateGroup, groupIndex (dateGroup.date)}
|
||||
{@const display =
|
||||
dateGroup.intersecting || !!dateGroup.assets.some((asset) => asset.id === $assetStore.pendingScrollAssetId)}
|
||||
{@const geometry = dateGroup.geometry!}
|
||||
|
||||
<div
|
||||
id="date-group"
|
||||
|
|
@ -119,7 +118,7 @@
|
|||
data-display={display}
|
||||
data-date-group={dateGroup.date}
|
||||
style:height={dateGroup.height + 'px'}
|
||||
style:width={geometry.containerWidth + 'px'}
|
||||
style:width={dateGroup.geometry.containerWidth + 'px'}
|
||||
style:overflow={'clip'}
|
||||
>
|
||||
{#if !display}
|
||||
|
|
@ -150,7 +149,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={geometry.containerWidth + 'px'}
|
||||
style:width={dateGroup.geometry.containerWidth + 'px'}
|
||||
>
|
||||
{#if !singleSelect && ((hoveredDateGroup == dateGroup.groupTitle && isMouseOverGroup) || assetInteraction.selectedGroup.has(dateGroup.groupTitle))}
|
||||
<div
|
||||
|
|
@ -175,17 +174,11 @@
|
|||
<!-- Image grid -->
|
||||
<div
|
||||
class="relative overflow-clip"
|
||||
style:height={geometry.containerHeight + 'px'}
|
||||
style:width={geometry.containerWidth + 'px'}
|
||||
style:height={dateGroup.geometry.containerHeight + 'px'}
|
||||
style:width={dateGroup.geometry.containerWidth + 'px'}
|
||||
>
|
||||
{#each dateGroup.assets as asset, i (asset.id)}
|
||||
{@const isSmallGroup = dateGroup.assets.length <= SMALL_GROUP_THRESHOLD}
|
||||
<!-- 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)}
|
||||
|
||||
{#each dateGroup.assets as asset, index (asset.id)}
|
||||
{@const box = dateGroup.geometry.boxes[index]}
|
||||
<!-- update ASSET_GRID_PADDING-->
|
||||
<div
|
||||
use:intersectionObserver={{
|
||||
|
|
@ -197,10 +190,10 @@
|
|||
}}
|
||||
data-asset-id={asset.id}
|
||||
class="absolute"
|
||||
style:top={top + 'px'}
|
||||
style:left={left + 'px'}
|
||||
style:width={width + 'px'}
|
||||
style:height={height + 'px'}
|
||||
style:width={box.width + 'px'}
|
||||
style:height={box.height + 'px'}
|
||||
style:top={box.top + 'px'}
|
||||
style:left={box.left + 'px'}
|
||||
>
|
||||
<Thumbnail
|
||||
{dateGroup}
|
||||
|
|
@ -222,9 +215,8 @@
|
|||
selected={assetInteraction.selectedAssets.has(asset) || $assetStore.albumAssets.has(asset.id)}
|
||||
selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
|
||||
disabled={$assetStore.albumAssets.has(asset.id)}
|
||||
thumbnailWidth={width}
|
||||
thumbnailHeight={height}
|
||||
eagerThumbhash={isSmallGroup}
|
||||
thumbnailWidth={box.width}
|
||||
thumbnailHeight={box.height}
|
||||
/>
|
||||
</div>
|
||||
{/each}
|
||||
|
|
|
|||
|
|
@ -20,7 +20,7 @@
|
|||
} from '$lib/utils/timeline-util';
|
||||
import { TUNABLES } from '$lib/utils/tunables';
|
||||
import type { AlbumResponseDto, AssetResponseDto, PersonResponseDto } from '@immich/sdk';
|
||||
import { debounce, throttle } from 'lodash-es';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { onDestroy, onMount, type Snippet } from 'svelte';
|
||||
import Portal from '../shared-components/portal/portal.svelte';
|
||||
import Scrubber from '../shared-components/scrubber/scrubber.svelte';
|
||||
|
|
@ -81,9 +81,8 @@
|
|||
|
||||
let { isViewing: showAssetViewer, asset: viewingAsset, preloadAssets, gridScrollTarget } = assetViewingStore;
|
||||
|
||||
// this does *not* need to be reactive and making it reactive causes expensive repeated updates
|
||||
// svelte-ignore non_reactive_update
|
||||
let safeViewport: ViewportXY = { width: 0, height: 0, x: 0, y: 0 };
|
||||
const viewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
|
||||
const safeViewport: ViewportXY = $state({ width: 0, height: 0, x: 0, y: 0 });
|
||||
|
||||
const componentId = generateId();
|
||||
let element: HTMLElement | undefined = $state();
|
||||
|
|
@ -104,7 +103,7 @@
|
|||
let leadout = $state(false);
|
||||
|
||||
const {
|
||||
ASSET_GRID: { NAVIGATE_ON_ASSET_IN_VIEW, LARGE_BUCKET_THRESHOLD, LARGE_BUCKET_DEBOUNCE_MS },
|
||||
ASSET_GRID: { NAVIGATE_ON_ASSET_IN_VIEW },
|
||||
BUCKET: {
|
||||
INTERSECTION_ROOT_TOP: BUCKET_INTERSECTION_ROOT_TOP,
|
||||
INTERSECTION_ROOT_BOTTOM: BUCKET_INTERSECTION_ROOT_BOTTOM,
|
||||
|
|
@ -115,6 +114,14 @@
|
|||
},
|
||||
} = TUNABLES;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
const completeNav = () => {
|
||||
navigating = false;
|
||||
if (internalScroll) {
|
||||
|
|
@ -228,14 +235,6 @@
|
|||
};
|
||||
|
||||
onMount(() => {
|
||||
if (element) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
safeViewport.height = rect.height;
|
||||
safeViewport.width = rect.width;
|
||||
safeViewport.x = rect.x;
|
||||
safeViewport.y = rect.y;
|
||||
}
|
||||
|
||||
void $assetStore
|
||||
.init({ bucketListener })
|
||||
.then(() => ($assetStore.connect(), $assetStore.updateViewport(safeViewport)));
|
||||
|
|
@ -260,6 +259,8 @@
|
|||
}
|
||||
return offset;
|
||||
}
|
||||
const _updateViewport = () => void $assetStore.updateViewport(safeViewport);
|
||||
const updateViewport = throttle(_updateViewport, 16);
|
||||
|
||||
const getMaxScrollPercent = () =>
|
||||
($assetStore.timelineHeight + bottomSectionHeight + topSectionHeight - safeViewport.height) /
|
||||
|
|
@ -743,8 +744,23 @@
|
|||
}
|
||||
});
|
||||
|
||||
let largeBucketMode = false;
|
||||
let updateViewport = debounce(() => $assetStore.updateViewport(safeViewport), 8);
|
||||
$effect(() => {
|
||||
if (element && isViewportOrigin()) {
|
||||
const rect = element.getBoundingClientRect();
|
||||
viewport.height = rect.height;
|
||||
viewport.width = rect.width;
|
||||
viewport.x = rect.x;
|
||||
viewport.y = rect.y;
|
||||
}
|
||||
if (!isViewportOrigin() && !isEqual(viewport, safeViewport)) {
|
||||
safeViewport.height = viewport.height;
|
||||
safeViewport.width = viewport.width;
|
||||
safeViewport.x = viewport.x;
|
||||
safeViewport.y = viewport.y;
|
||||
updateViewport();
|
||||
}
|
||||
});
|
||||
|
||||
let shortcutList = $derived(
|
||||
(() => {
|
||||
if ($isSearchEnabled || $showAssetViewer) {
|
||||
|
|
@ -827,21 +843,7 @@
|
|||
id="asset-grid"
|
||||
class="scrollbar-hidden h-full overflow-y-auto outline-none {isEmpty ? 'm-0' : 'ml-4 tall:ml-0 mr-[60px]'}"
|
||||
tabindex="-1"
|
||||
use:resizeObserver={({ width, height }) => {
|
||||
if (!largeBucketMode && assetStore.maxBucketAssets >= LARGE_BUCKET_THRESHOLD) {
|
||||
largeBucketMode = true;
|
||||
// Each viewport update causes each asset to re-decode both the thumbhash and the thumbnail.
|
||||
// This is because the thumbnail components are destroyed and re-mounted, possibly because of the intersection observer.
|
||||
// For larger buckets, this can lead to freezing and a poor user experience.
|
||||
// As a mitigation, we aggressively debounce the viewport update to reduce the number of these events.
|
||||
updateViewport = debounce(() => $assetStore.updateViewport(safeViewport), LARGE_BUCKET_DEBOUNCE_MS, {
|
||||
leading: false,
|
||||
trailing: true,
|
||||
});
|
||||
}
|
||||
safeViewport = { width, height, x: safeViewport.x, y: safeViewport.y };
|
||||
void updateViewport();
|
||||
}}
|
||||
use:resizeObserver={({ height, width }) => ((viewport.width = width), (viewport.height = height))}
|
||||
bind:this={element}
|
||||
onscroll={() => ((assetStore.lastScrollTime = Date.now()), handleTimelineScroll())}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -72,7 +72,7 @@
|
|||
<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'}
|
||||
style:width={dateGroup.geometry.containerWidth + 'px'}
|
||||
>
|
||||
<span class="w-full truncate first-letter:capitalize">
|
||||
{dateGroup.groupTitle}
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -8,11 +8,13 @@
|
|||
import type { Viewport } from '$lib/stores/assets.store';
|
||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||
import { deleteAssets } from '$lib/utils/actions';
|
||||
import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
|
||||
import { archiveAssets, cancelMultiselect, getAssetRatio } from '$lib/utils/asset-utils';
|
||||
import { featureFlags } from '$lib/stores/server-config.store';
|
||||
import { handleError } from '$lib/utils/handle-error';
|
||||
import { navigate } from '$lib/utils/navigation';
|
||||
import { calculateWidth } from '$lib/utils/timeline-util';
|
||||
import { type AssetResponseDto } from '@immich/sdk';
|
||||
import justifiedLayout from 'justified-layout';
|
||||
import { t } from 'svelte-i18n';
|
||||
import AssetViewer from '../../asset-viewer/asset-viewer.svelte';
|
||||
import ShowShortcuts from '../show-shortcuts.svelte';
|
||||
|
|
@ -307,15 +309,25 @@
|
|||
let isTrashEnabled = $derived($featureFlags.loaded && $featureFlags.trash);
|
||||
let idsSelectedAssets = $derived(assetInteraction.selectedAssetsArray.map(({ id }) => id));
|
||||
|
||||
let geometry = $derived.by(async () => {
|
||||
const { getJustifiedLayoutFromAssets } = await import('$lib/utils/layout-utils');
|
||||
return getJustifiedLayoutFromAssets(assets, {
|
||||
spacing: 2,
|
||||
heightTolerance: 0.15,
|
||||
rowHeight: 235,
|
||||
rowWidth: Math.floor(viewport.width),
|
||||
});
|
||||
});
|
||||
let geometry = $derived(
|
||||
(() => {
|
||||
const justifiedLayoutResult = justifiedLayout(
|
||||
assets.map((asset) => getAssetRatio(asset)),
|
||||
{
|
||||
boxSpacing: 2,
|
||||
containerWidth: Math.floor(viewport.width),
|
||||
containerPadding: 0,
|
||||
targetRowHeightTolerance: 0.15,
|
||||
targetRowHeight: 235,
|
||||
},
|
||||
);
|
||||
|
||||
return {
|
||||
...justifiedLayoutResult,
|
||||
containerWidth: calculateWidth(justifiedLayoutResult.boxes),
|
||||
};
|
||||
})(),
|
||||
);
|
||||
|
||||
$effect(() => {
|
||||
if (!lastAssetMouseEvent) {
|
||||
|
|
@ -351,49 +363,43 @@
|
|||
{/if}
|
||||
|
||||
{#if assets.length > 0}
|
||||
{#await geometry then geometry}
|
||||
<div class="relative" style="height: {geometry.containerHeight}px;width: {geometry.containerWidth}px ">
|
||||
{#each assets as asset, i}
|
||||
{@const top = geometry.getTop(i)}
|
||||
{@const left = geometry.getLeft(i)}
|
||||
{@const width = geometry.getWidth(i)}
|
||||
{@const height = geometry.getHeight(i)}
|
||||
|
||||
<div
|
||||
class="absolute"
|
||||
style="width: {width}px; height: {height}px; top: {top}px; left: {left}px"
|
||||
title={showAssetName ? asset.originalFileName : ''}
|
||||
>
|
||||
<Thumbnail
|
||||
readonly={disableAssetSelect}
|
||||
onClick={(asset) => {
|
||||
if (assetInteraction.selectionActive) {
|
||||
handleSelectAssets(asset);
|
||||
return;
|
||||
}
|
||||
void viewAssetHandler(asset);
|
||||
}}
|
||||
onSelect={(asset) => handleSelectAssets(asset)}
|
||||
onMouseEvent={() => assetMouseEventHandler(asset)}
|
||||
onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)}
|
||||
{showArchiveIcon}
|
||||
{asset}
|
||||
selected={assetInteraction.selectedAssets.has(asset)}
|
||||
selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
|
||||
thumbnailWidth={width}
|
||||
thumbnailHeight={height}
|
||||
/>
|
||||
{#if showAssetName}
|
||||
<div
|
||||
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-gradient-to-t bg-slate-50/75 overflow-clip text-ellipsis whitespace-pre-wrap"
|
||||
>
|
||||
{asset.originalFileName}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/await}
|
||||
<div class="relative" style="height: {geometry.containerHeight}px;width: {geometry.containerWidth}px ">
|
||||
{#each assets as asset, i (i)}
|
||||
<div
|
||||
class="absolute"
|
||||
style="width: {geometry.boxes[i].width}px; height: {geometry.boxes[i].height}px; top: {geometry.boxes[i]
|
||||
.top}px; left: {geometry.boxes[i].left}px"
|
||||
title={showAssetName ? asset.originalFileName : ''}
|
||||
>
|
||||
<Thumbnail
|
||||
readonly={disableAssetSelect}
|
||||
onClick={(asset) => {
|
||||
if (assetInteraction.selectionActive) {
|
||||
handleSelectAssets(asset);
|
||||
return;
|
||||
}
|
||||
void viewAssetHandler(asset);
|
||||
}}
|
||||
onSelect={(asset) => handleSelectAssets(asset)}
|
||||
onMouseEvent={() => assetMouseEventHandler(asset)}
|
||||
onIntersected={() => (i === Math.max(1, assets.length - 7) ? onIntersected?.() : void 0)}
|
||||
{showArchiveIcon}
|
||||
{asset}
|
||||
selected={assetInteraction.selectedAssets.has(asset)}
|
||||
selectionCandidate={assetInteraction.assetSelectionCandidates.has(asset)}
|
||||
thumbnailWidth={geometry.boxes[i].width}
|
||||
thumbnailHeight={geometry.boxes[i].height}
|
||||
/>
|
||||
{#if showAssetName}
|
||||
<div
|
||||
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-gradient-to-t bg-slate-50/75 overflow-clip text-ellipsis whitespace-pre-wrap"
|
||||
>
|
||||
{asset.originalFileName}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<!-- Overlay Asset Viewer -->
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue