From d7d3b8dfece2272ddd54b52b39e29fa32b37582e Mon Sep 17 00:00:00 2001 From: Min Idzelis Date: Thu, 5 Sep 2024 07:29:07 -0700 Subject: [PATCH] fix: flash bug on tag (#12332) * fix flash bug on tag * fix lint --------- Co-authored-by: Alex --- docs/docs/guides/template-backup-script.md | 2 +- web/src/lib/actions/intersection-observer.ts | 30 +++------ .../photos-page/asset-date-group.svelte | 3 +- .../components/photos-page/asset-grid.svelte | 3 +- web/src/lib/stores/assets.store.ts | 64 +++++++++++++++---- web/src/lib/utils/asset-store-task-manager.ts | 4 +- .../[[assetId=id]]/+page.svelte | 27 ++++---- 7 files changed, 79 insertions(+), 54 deletions(-) diff --git a/docs/docs/guides/template-backup-script.md b/docs/docs/guides/template-backup-script.md index 9777d00262..03c1a7a02b 100644 --- a/docs/docs/guides/template-backup-script.md +++ b/docs/docs/guides/template-backup-script.md @@ -78,4 +78,4 @@ borg mount "$REMOTE_HOST:$REMOTE_BACKUP_PATH"/immich-borg /tmp/immich-mountpoint cd /tmp/immich-mountpoint ``` -You can find available snapshots in seperate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint` +You can find available snapshots in separate sub-directories at `/tmp/immich-mountpoint`. Restore the files you need, and unmount the Borg repository using `borg umount /tmp/immich-mountpoint` diff --git a/web/src/lib/actions/intersection-observer.ts b/web/src/lib/actions/intersection-observer.ts index 222f76be63..700ae0c373 100644 --- a/web/src/lib/actions/intersection-observer.ts +++ b/web/src/lib/actions/intersection-observer.ts @@ -10,10 +10,10 @@ type TrackedProperties = { left?: string; }; type OnIntersectCallback = (entryOrElement: IntersectionObserverEntry | HTMLElement) => unknown; -type OnSeperateCallback = (element: HTMLElement) => unknown; +type OnSeparateCallback = (element: HTMLElement) => unknown; type IntersectionObserverActionProperties = { key?: string; - onSeparate?: OnSeperateCallback; + onSeparate?: OnSeparateCallback; onIntersect?: OnIntersectCallback; root?: Element | Document | null; @@ -22,8 +22,6 @@ type IntersectionObserverActionProperties = { right?: string; bottom?: string; left?: string; - - disabled?: boolean; }; type TaskKey = HTMLElement | string; @@ -92,11 +90,7 @@ function _intersectionObserver( element: HTMLElement, properties: IntersectionObserverActionProperties, ) { - if (properties.disabled) { - properties.onIntersect?.(element); - } else { - configure(key, element, properties); - } + configure(key, element, properties); return { update(properties: IntersectionObserverActionProperties) { const config = elementToConfig.get(key); @@ -106,20 +100,14 @@ function _intersectionObserver( if (isEquivalent(config, properties)) { return; } + configure(key, element, properties); }, destroy: () => { - if (properties.disabled) { - properties.onSeparate?.(element); - } else { - const config = elementToConfig.get(key); - const { observer, onSeparate } = config || {}; - observer?.unobserve(element); - elementToConfig.delete(key); - if (onSeparate) { - onSeparate?.(element); - } - } + const config = elementToConfig.get(key); + const { observer } = config || {}; + observer?.unobserve(element); + elementToConfig.delete(key); }, }; } @@ -148,5 +136,5 @@ export function intersectionObserver( }, }; } - return _intersectionObserver(element, element, properties); + return _intersectionObserver(properties.key || element, element, properties); } diff --git a/web/src/lib/components/photos-page/asset-date-group.svelte b/web/src/lib/components/photos-page/asset-date-group.svelte index 5cbc2e7dca..240b6c2ba2 100644 --- a/web/src/lib/components/photos-page/asset-date-group.svelte +++ b/web/src/lib/components/photos-page/asset-date-group.svelte @@ -35,7 +35,7 @@ $: dateGroups = bucket.dateGroups; const { - DATEGROUP: { INTERSECTION_DISABLED, INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM }, + DATEGROUP: { INTERSECTION_ROOT_TOP, INTERSECTION_ROOT_BOTTOM }, } = TUNABLES; /* TODO figure out a way to calculate this*/ const TITLE_HEIGHT = 51; @@ -116,7 +116,6 @@ top: INTERSECTION_ROOT_TOP, bottom: INTERSECTION_ROOT_BOTTOM, root: assetGridElement, - disabled: INTERSECTION_DISABLED, }} data-display={display} data-date-group={dateGroup.date} diff --git a/web/src/lib/components/photos-page/asset-grid.svelte b/web/src/lib/components/photos-page/asset-grid.svelte index 94e7803b97..f59911dbaf 100644 --- a/web/src/lib/components/photos-page/asset-grid.svelte +++ b/web/src/lib/components/photos-page/asset-grid.svelte @@ -804,12 +804,13 @@ class:invisible={showSkeleton} style:height={$assetStore.timelineHeight + 'px'} > - {#each $assetStore.buckets as bucket (bucket.bucketDate)} + {#each $assetStore.buckets as bucket (bucket.viewId)} {@const isPremeasure = preMeasure.includes(bucket)} {@const display = bucket.intersecting || bucket === $assetStore.pendingScrollBucket || isPremeasure}
handleIntersect(bucket), onSeparate: () => handleSeparate(bucket), top: BUCKET_INTERSECTION_ROOT_TOP, diff --git a/web/src/lib/stores/assets.store.ts b/web/src/lib/stores/assets.store.ts index 763d5b1874..ed6f9fdf04 100644 --- a/web/src/lib/stores/assets.store.ts +++ b/web/src/lib/stores/assets.store.ts @@ -2,6 +2,7 @@ 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 { AssetGridRouteSearchParams } from '$lib/utils/navigation'; import { calculateWidth, fromLocalDateTime, splitBucketIntoDateGroups, type DateGroup } from '$lib/utils/timeline-util'; import { TimeBucketSize, getAssetInfo, getTimeBucket, getTimeBuckets, type AssetResponseDto } from '@immich/sdk'; @@ -12,7 +13,6 @@ import { t } from 'svelte-i18n'; import { get, writable, type Unsubscriber } from 'svelte/store'; import { handleError } from '../utils/handle-error'; import { websocketEvents } from './websocket'; - type AssetApiGetTimeBucketsRequest = Parameters[0]; export type AssetStoreOptions = Omit; @@ -70,7 +70,10 @@ export class AssetBucket { Object.assign(this, props); this.init(); } - + /** The svelte key for this view model object */ + get viewId() { + return this.store.viewId + '-' + this.bucketDate; + } private init() { // create a promise, and store its resolve/reject callbacks. The loadedSignal callback // will be incoked when a bucket is loaded, fulfilling the promise. The canceledSignal @@ -205,21 +208,23 @@ export class AssetStore { private assetToBucket: Record = {}; private pendingChanges: PendingChange[] = []; private unsubscribers: Unsubscriber[] = []; - private options: AssetApiGetTimeBucketsRequest; + private options!: AssetApiGetTimeBucketsRequest; private viewport: Viewport = { 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; /** * A promise that resolves once the store is initialized. */ - taskManager = new AssetGridTaskManager(this); complete!: Promise; + taskManager = new AssetGridTaskManager(this); initialized = false; timelineHeight = 0; buckets: AssetBucket[] = []; @@ -234,13 +239,23 @@ export class AssetStore { options: AssetStoreOptions, private albumId?: string, ) { + this.setOptions(options); + this.createInitializationSignal(); + this.store$.set(this); + } + + private setOptions(options: AssetStoreOptions) { this.options = { ...options, size: TimeBucketSize.Month }; + } + + 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.initializedSignal = resolve; }); - this.store$.set(this); + // uncaught rejection go away + this.complete.catch(() => void 0); } private addPendingChanges(...changes: PendingChange[]) { @@ -273,6 +288,7 @@ export class AssetStore { for (const unsubscribe of this.unsubscribers) { unsubscribe(); } + this.unsubscribers = []; } private getPendingChangeBatches() { @@ -360,8 +376,10 @@ export class AssetStore { if (bucketListener) { this.addListener(bucketListener); } - // uncaught rejection go away - this.complete.catch(() => void 0); + await this.initialiazeTimeBuckets(); + } + + async initialiazeTimeBuckets() { this.timelineHeight = 0; this.buckets = []; this.assets = []; @@ -379,6 +397,27 @@ export class AssetStore { this.initialized = true; } + async updateOptions(options: AssetStoreOptions) { + if (!this.initialized) { + this.setOptions(options); + return; + } + // TODO: don't call updateObjects frequently after + // init - cancelation of the initialize tasks isn't + // performed right now, and will cause issues if + // multiple updateOptions() calls are interleved. + await this.complete; + this.taskManager.destroy(); + this.taskManager = new AssetGridTaskManager(this); + this.initialized = false; + this.viewId = generateId(); + this.createInitializationSignal(); + this.setOptions(options); + await this.initialiazeTimeBuckets(); + this.emit(true); + await this.initialLayout(true); + } + public destroy() { this.taskManager.destroy(); this.listeners = []; @@ -386,22 +425,21 @@ export class AssetStore { } async updateViewport(viewport: Viewport, force?: boolean) { - if (!this.initialized) { - return; - } if (viewport.height === 0 && viewport.width === 0) { return; } - if (!force && this.viewport.height === viewport.height && this.viewport.width === viewport.width) { return; } - + await this.complete; // changing width invalidates the actual height, and needs to be remeasured, since width changes causes // layout reflows. const changedWidth = this.viewport.width != viewport.width; this.viewport = { ...viewport }; + await this.initialLayout(changedWidth); + } + private async initialLayout(changedWidth: boolean) { for (const bucket of this.buckets) { this.updateGeometry(bucket, changedWidth); } @@ -410,7 +448,7 @@ export class AssetStore { const loaders = []; let height = 0; for (const bucket of this.buckets) { - if (height >= viewport.height) { + if (height >= this.viewport.height) { break; } height += bucket.bucketHeight; diff --git a/web/src/lib/utils/asset-store-task-manager.ts b/web/src/lib/utils/asset-store-task-manager.ts index e476738456..60004235f4 100644 --- a/web/src/lib/utils/asset-store-task-manager.ts +++ b/web/src/lib/utils/asset-store-task-manager.ts @@ -315,7 +315,7 @@ class IntersectionTask { return { task: execTask, cleanup }; } - trackSeperatedTask(componentId: string, task: Task) { + trackSeparatedTask(componentId: string, task: Task) { const execTask = () => { if (this.intersected) { return; @@ -363,7 +363,7 @@ class IntersectionTask { return; } - const { task, cleanup } = this.trackSeperatedTask(componentId, separated); + const { task, cleanup } = this.trackSeparatedTask(componentId, separated); this.internalTaskManager.queueSeparateTask({ task, cleanup, diff --git a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte index a5d3630aa0..ce91abb451 100644 --- a/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte +++ b/web/src/routes/(user)/tags/[[photos=photos]]/[[assetId=id]]/+page.svelte @@ -40,10 +40,16 @@ return Object.fromEntries(tags.map((tag) => [tag.value, tag])); }; + const assetStore = new AssetStore({}); + $: tags = data.tags; $: tagsMap = buildMap(tags); $: tag = currentPath ? tagsMap[currentPath] : null; + $: tagId = tag?.id; $: tree = buildTree(tags.map((tag) => tag.value)); + $: { + void assetStore.updateOptions({ tagId }); + } const handleNavigation = async (tag: string) => { await navigateToView(normalizeTreePath(`${data.path || ''}/${tag}`)); @@ -169,20 +175,13 @@
- {#key $page.url.href} - {#if tag} - - - - {:else} - - {/if} - {/key} + {#if tag} + + + + {:else} + + {/if}