Skip to content

Commit 4c69511

Browse files
authored
revert: "feat(web): wasm justified layout" (#19226)
1 parent 0684a3a commit 4c69511

File tree

13 files changed

+146
-107
lines changed

13 files changed

+146
-107
lines changed

web/eslint.config.js

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,6 @@ export default typescriptEslint.config(
118118
'unicorn/filename-case': 'off',
119119
'unicorn/prefer-top-level-await': 'off',
120120
'unicorn/import-style': 'off',
121-
'unicorn/no-for-loop': 'off',
122121
'svelte/button-has-type': 'error',
123122
'@typescript-eslint/await-thenable': 'error',
124123
'@typescript-eslint/no-floating-promises': 'error',

web/package-lock.json

Lines changed: 0 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

web/package.json

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@
2727
},
2828
"dependencies": {
2929
"@formatjs/icu-messageformat-parser": "^2.9.8",
30-
"@immich/justified-layout-wasm": "^0.3.0",
3130
"@immich/sdk": "file:../open-api/typescript-sdk",
3231
"@immich/ui": "^0.22.7",
3332
"@mapbox/mapbox-gl-rtl-text": "0.2.3",

web/src/lib/components/shared-components/gallery-viewer/gallery-viewer.svelte

Lines changed: 89 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { archiveAssets, cancelMultiselect } from '$lib/utils/asset-utils';
1717
import { moveFocus } from '$lib/utils/focus-util';
1818
import { handleError } from '$lib/utils/handle-error';
19-
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
19+
import { getJustifiedLayoutFromAssets, type CommonJustifiedLayout } from '$lib/utils/layout-utils';
2020
import { navigate } from '$lib/utils/navigation';
2121
import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util';
2222
import { AssetVisibility, type AssetResponseDto } from '@immich/sdk';
@@ -27,7 +27,7 @@
2727
import Portal from '../portal/portal.svelte';
2828
2929
interface Props {
30-
assets: TimelineAsset[] | AssetResponseDto[];
30+
assets: (TimelineAsset | AssetResponseDto)[];
3131
assetInteraction: AssetInteraction;
3232
disableAssetSelect?: boolean;
3333
showArchiveIcon?: boolean;
@@ -62,39 +62,91 @@
6262
6363
let { isViewing: isViewerOpen, asset: viewingAsset, setAssetId } = assetViewingStore;
6464
65-
const geometry = $derived(
66-
getJustifiedLayoutFromAssets(assets, {
65+
let geometry: CommonJustifiedLayout | undefined = $state();
66+
67+
$effect(() => {
68+
const _assets = assets;
69+
updateSlidingWindow();
70+
71+
const rowWidth = Math.floor(viewport.width);
72+
const rowHeight = rowWidth < 850 ? 100 : 235;
73+
74+
geometry = getJustifiedLayoutFromAssets(_assets, {
6775
spacing: 2,
68-
heightTolerance: 0.3,
69-
rowHeight: Math.floor(viewport.width) < 850 ? 100 : 235,
70-
rowWidth: Math.floor(viewport.width),
71-
}),
72-
);
76+
heightTolerance: 0.15,
77+
rowHeight,
78+
rowWidth,
79+
});
80+
});
81+
82+
let assetLayouts = $derived.by(() => {
83+
const assetLayout = [];
84+
let containerHeight = 0;
85+
let containerWidth = 0;
86+
if (geometry) {
87+
containerHeight = geometry.containerHeight;
88+
containerWidth = geometry.containerWidth;
89+
for (const [index, asset] of assets.entries()) {
90+
const top = geometry.getTop(index);
91+
const left = geometry.getLeft(index);
92+
const width = geometry.getWidth(index);
93+
const height = geometry.getHeight(index);
94+
95+
const layoutTopWithOffset = top + pageHeaderOffset;
96+
const layoutBottom = layoutTopWithOffset + height;
97+
98+
const display = layoutTopWithOffset < slidingWindow.bottom && layoutBottom > slidingWindow.top;
99+
100+
const layout = {
101+
asset,
102+
top,
103+
left,
104+
width,
105+
height,
106+
display,
107+
};
108+
109+
assetLayout.push(layout);
110+
}
111+
}
112+
113+
return {
114+
assetLayout,
115+
containerHeight,
116+
containerWidth,
117+
};
118+
});
73119
74120
let currentViewAssetIndex = 0;
75121
let shiftKeyIsDown = $state(false);
76122
let lastAssetMouseEvent: TimelineAsset | null = $state(null);
77-
let slidingTop = $state(0);
78-
let slidingBottom = $state(0);
123+
let slidingWindow = $state({ top: 0, bottom: 0 });
79124
80125
const updateSlidingWindow = () => {
81126
const v = $state.snapshot(viewport);
82127
const top = (document.scrollingElement?.scrollTop || 0) - slidingWindowOffset;
83-
slidingTop = top;
84-
slidingBottom = top + v.height;
128+
const bottom = top + v.height;
129+
const w = {
130+
top,
131+
bottom,
132+
};
133+
slidingWindow = w;
85134
};
86-
$effect(updateSlidingWindow);
87135
const debouncedOnIntersected = debounce(() => onIntersected?.(), 750, { maxWait: 100, leading: true });
88136
89137
let lastIntersectedHeight = 0;
90138
$effect(() => {
91139
// notify we got to (near) the end of scroll
92140
const scrollPercentage =
93-
(slidingBottom - viewport.height) / (viewport.height - (document.scrollingElement?.clientHeight || 0));
94-
95-
if (scrollPercentage > 0.9 && lastIntersectedHeight !== geometry.containerHeight) {
96-
debouncedOnIntersected();
97-
lastIntersectedHeight = geometry.containerHeight;
141+
((slidingWindow.bottom - viewport.height) / (viewport.height - (document.scrollingElement?.clientHeight || 0))) *
142+
100;
143+
144+
if (scrollPercentage > 90) {
145+
const intersectedHeight = geometry?.containerHeight || 0;
146+
if (lastIntersectedHeight !== intersectedHeight) {
147+
debouncedOnIntersected();
148+
lastIntersectedHeight = intersectedHeight;
149+
}
98150
}
99151
});
100152
const viewAssetHandler = async (asset: TimelineAsset) => {
@@ -204,7 +256,7 @@
204256
isShowDeleteConfirmation = false;
205257
await deleteAssets(
206258
!(isTrashEnabled && !force),
207-
(assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id)) as TimelineAsset[]),
259+
(assetIds) => (assets = assets.filter((asset) => !assetIds.includes(asset.id))),
208260
assetInteraction.selectedAssets,
209261
onReload,
210262
);
@@ -217,7 +269,7 @@
217269
assetInteraction.isAllArchived ? AssetVisibility.Timeline : AssetVisibility.Archive,
218270
);
219271
if (ids) {
220-
assets = assets.filter((asset) => !ids.includes(asset.id)) as TimelineAsset[];
272+
assets = assets.filter((asset) => !ids.includes(asset.id));
221273
deselectAllAssets();
222274
}
223275
};
@@ -402,7 +454,7 @@
402454
onkeyup={onKeyUp}
403455
onselectstart={onSelectStart}
404456
use:shortcuts={shortcutList}
405-
onscroll={updateSlidingWindow}
457+
onscroll={() => updateSlidingWindow()}
406458
/>
407459

408460
{#if isShowDeleteConfirmation}
@@ -416,12 +468,13 @@
416468
{#if assets.length > 0}
417469
<div
418470
style:position="relative"
419-
style:height={geometry.containerHeight + 'px'}
420-
style:width={geometry.containerWidth + 'px'}
471+
style:height={assetLayouts.containerHeight + 'px'}
472+
style:width={assetLayouts.containerWidth - 1 + 'px'}
421473
>
422-
{#each assets as asset, i (asset.id + i)}
423-
{#if geometry.getTop(i) + pageHeaderOffset < slidingBottom && geometry.getTop(i) + pageHeaderOffset + geometry.getHeight(i) > slidingTop}
424-
{@const layout = geometry.getPosition(i)}
474+
{#each assetLayouts.assetLayout as layout, layoutIndex (layout.asset.id + '-' + layoutIndex)}
475+
{@const currentAsset = layout.asset}
476+
477+
{#if layout.display}
425478
<div
426479
class="absolute"
427480
style:overflow="clip"
@@ -431,25 +484,25 @@
431484
readonly={disableAssetSelect}
432485
onClick={() => {
433486
if (assetInteraction.selectionActive) {
434-
handleSelectAssets(toTimelineAsset(asset));
487+
handleSelectAssets(toTimelineAsset(currentAsset));
435488
return;
436489
}
437-
void viewAssetHandler(toTimelineAsset(asset));
490+
void viewAssetHandler(toTimelineAsset(currentAsset));
438491
}}
439-
onSelect={() => handleSelectAssets(toTimelineAsset(asset))}
440-
onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(asset))}
492+
onSelect={() => handleSelectAssets(toTimelineAsset(currentAsset))}
493+
onMouseEvent={() => assetMouseEventHandler(toTimelineAsset(currentAsset))}
441494
{showArchiveIcon}
442-
asset={toTimelineAsset(asset)}
443-
selected={assetInteraction.hasSelectedAsset(asset.id)}
444-
selectionCandidate={assetInteraction.hasSelectionCandidate(asset.id)}
495+
asset={toTimelineAsset(currentAsset)}
496+
selected={assetInteraction.hasSelectedAsset(currentAsset.id)}
497+
selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)}
445498
thumbnailWidth={layout.width}
446499
thumbnailHeight={layout.height}
447500
/>
448-
{#if showAssetName && !isTimelineAsset(asset)}
501+
{#if showAssetName && !isTimelineAsset(currentAsset)}
449502
<div
450503
class="absolute text-center p-1 text-xs font-mono font-semibold w-full bottom-0 bg-linear-to-t bg-slate-50/75 dark:bg-slate-800/75 overflow-clip text-ellipsis whitespace-pre-wrap"
451504
>
452-
{asset.originalFileName}
505+
{currentAsset.originalFileName}
453506
</div>
454507
{/if}
455508
</div>

web/src/lib/managers/timeline-manager/day-group.svelte.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { AssetOrder } from '@immich/sdk';
22

33
import type { CommonLayoutOptions } from '$lib/utils/layout-utils';
4-
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
4+
import { getJustifiedLayoutFromAssets, getPosition } from '$lib/utils/layout-utils';
55
import { plainDateTimeCompare } from '$lib/utils/timeline-util';
66

77
import type { MonthGroup } from './month-group.svelte';
@@ -153,7 +153,8 @@ export class DayGroup {
153153
this.width = geometry.containerWidth;
154154
this.height = assets.length === 0 ? 0 : geometry.containerHeight;
155155
for (let i = 0; i < this.viewerAssets.length; i++) {
156-
this.viewerAssets[i].position = geometry.getPosition(i);
156+
const position = getPosition(geometry, i);
157+
this.viewerAssets[i].position = position;
157158
}
158159
}
159160

web/src/lib/managers/timeline-manager/timeline-manager.svelte.spec.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,8 @@ import { sdkMock } from '$lib/__mocks__/sdk.mock';
22
import { getMonthGroupByDate } from '$lib/managers/timeline-manager/internal/search-support.svelte';
33
import { AbortError } from '$lib/utils';
44
import { fromISODateTimeUTCToObject } from '$lib/utils/timeline-util';
5-
import { initSync } from '@immich/justified-layout-wasm';
65
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
76
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
8-
import { readFile } from 'node:fs/promises';
97
import { TimelineManager } from './timeline-manager.svelte';
108
import type { TimelineAsset } from './types';
119

@@ -25,12 +23,6 @@ function deriveLocalDateTimeFromFileCreatedAt(arg: TimelineAsset): TimelineAsset
2523
}
2624

2725
describe('TimelineManager', () => {
28-
beforeAll(async () => {
29-
// needed for Node.js
30-
const file = await readFile('node_modules/@immich/justified-layout-wasm/pkg/justified-layout-wasm_bg.wasm');
31-
initSync({ module: file });
32-
});
33-
3426
beforeEach(() => {
3527
vi.resetAllMocks();
3628
});
@@ -88,15 +80,15 @@ describe('TimelineManager', () => {
8880

8981
expect(plainMonths).toEqual(
9082
expect.arrayContaining([
91-
expect.objectContaining({ year: 2024, month: 3, height: 353.5 }),
92-
expect.objectContaining({ year: 2024, month: 2, height: 7786.452_636_718_75 }),
83+
expect.objectContaining({ year: 2024, month: 3, height: 185.5 }),
84+
expect.objectContaining({ year: 2024, month: 2, height: 12_016 }),
9385
expect.objectContaining({ year: 2024, month: 1, height: 286 }),
9486
]),
9587
);
9688
});
9789

9890
it('calculates timeline height', () => {
99-
expect(timelineManager.timelineHeight).toBe(8425.952_636_718_75);
91+
expect(timelineManager.timelineHeight).toBe(12_487.5);
10092
});
10193
});
10294

web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -377,11 +377,13 @@ export class TimelineManager {
377377
}
378378

379379
createLayoutOptions() {
380+
const viewportWidth = this.viewportWidth;
381+
380382
return {
381383
spacing: 2,
382-
heightTolerance: 0.3,
384+
heightTolerance: 0.15,
383385
rowHeight: this.#rowHeight,
384-
rowWidth: Math.floor(this.viewportWidth),
386+
rowWidth: Math.floor(viewportWidth),
385387
};
386388
}
387389

web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ export class ViewerAsset {
1818
return calculateViewerAssetIntersecting(store, positionTop, this.position.height);
1919
});
2020

21-
position: CommonPosition | undefined = $state.raw();
21+
position: CommonPosition | undefined = $state();
2222
asset: TimelineAsset = <TimelineAsset>$state();
2323
id: string = $derived(this.asset.id);
2424

0 commit comments

Comments
 (0)