refactor(web): replace intersection booleans with enum (#27306)

Change-Id: I0c9703d5960031142ae47fef23805e0a6a6a6964
This commit is contained in:
Min Idzelis
2026-03-27 08:37:12 -04:00
committed by GitHub
parent 4b9ebc2cff
commit 2d950db940
10 changed files with 121 additions and 99 deletions
@@ -1,5 +1,6 @@
<script lang="ts"> <script lang="ts">
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte';
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte'; import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte'; import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
import { uploadAssetsStore } from '$lib/stores/upload'; import { uploadAssetsStore } from '$lib/stores/upload';
@@ -30,15 +31,11 @@
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150); const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100); const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
const filterIntersecting = <T extends { intersecting: boolean }>(intersectables: T[]) => {
return intersectables.filter(({ intersecting }) => intersecting);
};
</script> </script>
<!-- Image grid --> <!-- Image grid -->
<div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}> <div data-image-grid class="relative overflow-clip" style:height={height + 'px'} style:width={width + 'px'}>
{#each filterIntersecting(viewerAssets) as viewerAsset (viewerAsset.id)} {#each filterIsInOrNearViewport(viewerAssets) as viewerAsset (viewerAsset.id)}
{@const position = viewerAsset.position!} {@const position = viewerAsset.position!}
{@const asset = viewerAsset.asset!} {@const asset = viewerAsset.asset!}
+12 -7
View File
@@ -3,7 +3,7 @@
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte'; import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte'; import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types'; import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte'; import { assetsSnapshot, filterIsInOrNearViewport } from '$lib/managers/timeline-manager/utils.svelte';
import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte'; import type { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte'; import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { uploadAssetsStore } from '$lib/stores/upload'; import { uploadAssetsStore } from '$lib/stores/upload';
@@ -14,7 +14,16 @@
import type { Snippet } from 'svelte'; import type { Snippet } from 'svelte';
type Props = { type Props = {
thumbnail: Snippet<[{ asset: TimelineAsset; position: CommonPosition; dayGroup: DayGroup; groupIndex: number }]>; thumbnail: Snippet<
[
{
asset: TimelineAsset;
position: CommonPosition;
dayGroup: DayGroup;
groupIndex: number;
},
]
>;
customThumbnailLayout?: Snippet<[TimelineAsset]>; customThumbnailLayout?: Snippet<[TimelineAsset]>;
singleSelect: boolean; singleSelect: boolean;
assetInteraction: AssetInteraction; assetInteraction: AssetInteraction;
@@ -37,10 +46,6 @@
const transitionDuration = $derived(monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150); const transitionDuration = $derived(monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150);
const filterIntersecting = <T extends { intersecting: boolean }>(intersectables: T[]) => {
return intersectables.filter(({ intersecting }) => intersecting);
};
const getDayGroupFullDate = (dayGroup: DayGroup): string => { const getDayGroupFullDate = (dayGroup: DayGroup): string => {
const { month, year } = dayGroup.monthGroup.yearMonth; const { month, year } = dayGroup.monthGroup.yearMonth;
const date = fromTimelinePlainDate({ const date = fromTimelinePlainDate({
@@ -52,7 +57,7 @@
}; };
</script> </script>
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)} {#each filterIsInOrNearViewport(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
{@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)} {@const isDayGroupSelected = assetInteraction.selectedGroup.has(dayGroup.groupTitle)}
<!-- svelte-ignore a11y_no_static_element_interactions --> <!-- svelte-ignore a11y_no_static_element_interactions -->
<section <section
@@ -642,7 +642,7 @@
</section> </section>
{#each timelineManager.months as monthGroup (monthGroup.viewId)} {#each timelineManager.months as monthGroup (monthGroup.viewId)}
{@const display = monthGroup.intersecting} {@const isInOrNearViewport = monthGroup.isInOrNearViewport}
{@const absoluteHeight = monthGroup.top} {@const absoluteHeight = monthGroup.top}
{#if !monthGroup.isLoaded} {#if !monthGroup.isLoaded}
@@ -654,7 +654,7 @@
> >
<Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} /> <Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} />
</div> </div>
{:else if display} {:else if isInOrNearViewport}
<div <div
class="month-group" class="month-group"
style:height={monthGroup.height + 'px'} style:height={monthGroup.height + 'px'}
@@ -138,7 +138,7 @@ export abstract class VirtualScrollManager {
return this.viewportWidth === 0 || this.viewportHeight === 0; return this.viewportWidth === 0 || this.viewportHeight === 0;
} }
protected updateIntersections(): void {} protected updateViewportProximities(): void {}
protected updateViewportGeometry(_: boolean) {} protected updateViewportGeometry(_: boolean) {}
@@ -156,12 +156,12 @@ export abstract class VirtualScrollManager {
const scrollTop = this.scrollTop; const scrollTop = this.scrollTop;
if (this.#scrollTop !== scrollTop) { if (this.#scrollTop !== scrollTop) {
this.#scrollTop = scrollTop; this.#scrollTop = scrollTop;
this.updateIntersections(); this.updateViewportProximities();
} }
} }
refreshLayout() { refreshLayout() {
this.updateIntersections(); this.updateViewportProximities();
} }
destroy(): void {} destroy(): void {}
@@ -18,7 +18,7 @@ export class DayGroup {
height = $state(0); height = $state(0);
width = $state(0); width = $state(0);
intersecting = $derived.by(() => this.viewerAssets.some((viewAsset) => viewAsset.intersecting)); isInOrNearViewport = $derived.by(() => this.viewerAssets.some((viewAsset) => viewAsset.isInOrNearViewport));
#top: number = $state(0); #top: number = $state(0);
#start: number = $state(0); #start: number = $state(0);
@@ -137,7 +137,7 @@ export class DayGroup {
} }
layout(options: CommonLayoutOptions, noDefer: boolean) { layout(options: CommonLayoutOptions, noDefer: boolean) {
if (!noDefer && !this.monthGroup.intersecting && !this.monthGroup.timelineManager.isScrollingOnLoad) { if (!noDefer && !this.monthGroup.isInOrNearViewport && !this.monthGroup.timelineManager.isScrollingOnLoad) {
this.#deferredLayout = true; this.#deferredLayout = true;
return; return;
} }
@@ -6,68 +6,64 @@ const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM }, TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
} = TUNABLES; } = TUNABLES;
export function updateIntersectionMonthGroup(timelineManager: TimelineManager, month: MonthGroup) { export function isIntersecting(regionTop: number, regionBottom: number, otherTop: number, otherBottom: number) {
const actuallyIntersecting = calculateMonthGroupIntersecting(timelineManager, month, 0, 0); return (
let preIntersecting = false; (regionTop >= otherTop && regionTop < otherBottom) ||
if (!actuallyIntersecting) { (regionBottom >= otherTop && regionBottom < otherBottom) ||
preIntersecting = calculateMonthGroupIntersecting( (regionTop < otherTop && regionBottom >= otherBottom)
timelineManager,
month,
INTERSECTION_EXPAND_TOP,
INTERSECTION_EXPAND_BOTTOM,
); );
} }
month.intersecting = actuallyIntersecting || preIntersecting;
month.actuallyIntersecting = actuallyIntersecting; export enum ViewportProximity {
if (preIntersecting || actuallyIntersecting) { FarFromViewport,
NearViewport,
InViewport,
}
export function isInViewport(state: ViewportProximity): boolean {
return state === ViewportProximity.InViewport;
}
export function isInOrNearViewport(state: ViewportProximity): boolean {
return state !== ViewportProximity.FarFromViewport;
}
function calculateViewportProximity(regionTop: number, regionBottom: number, windowTop: number, windowBottom: number) {
if (regionBottom < windowTop - INTERSECTION_EXPAND_TOP || regionTop >= windowBottom + INTERSECTION_EXPAND_BOTTOM) {
return ViewportProximity.FarFromViewport;
}
if (regionBottom < windowTop || regionTop >= windowBottom) {
return ViewportProximity.NearViewport;
}
return ViewportProximity.InViewport;
}
export function updateMonthGroupViewportProximity(timelineManager: TimelineManager, month: MonthGroup) {
const proximity = calculateViewportProximity(
month.top,
month.top + month.height,
timelineManager.visibleWindow.top,
timelineManager.visibleWindow.bottom,
);
month.viewportProximity = proximity;
if (isInOrNearViewport(proximity)) {
timelineManager.clearDeferredLayout(month); timelineManager.clearDeferredLayout(month);
} }
} }
/** export function calculateViewerAssetViewportProximity(
* General function to check if a rectangular region intersects with a window.
* @param regionTop - Top position of the region to check
* @param regionBottom - Bottom position of the region to check
* @param windowTop - Top position of the window
* @param windowBottom - Bottom position of the window
* @returns true if the region intersects with the window
*/
export function isIntersecting(regionTop: number, regionBottom: number, windowTop: number, windowBottom: number) {
return (
(regionTop >= windowTop && regionTop < windowBottom) ||
(regionBottom >= windowTop && regionBottom < windowBottom) ||
(regionTop < windowTop && regionBottom >= windowBottom)
);
}
export function calculateMonthGroupIntersecting(
timelineManager: TimelineManager,
monthGroup: MonthGroup,
expandTop: number,
expandBottom: number,
) {
const monthGroupTop = monthGroup.top;
const monthGroupBottom = monthGroupTop + monthGroup.height;
const topWindow = timelineManager.visibleWindow.top - expandTop;
const bottomWindow = timelineManager.visibleWindow.bottom + expandBottom;
return isIntersecting(monthGroupTop, monthGroupBottom, topWindow, bottomWindow);
}
/**
* Calculate intersection for viewer assets with additional parameters like header height
*/
export function calculateViewerAssetIntersecting(
timelineManager: TimelineManager, timelineManager: TimelineManager,
positionTop: number, positionTop: number,
positionHeight: number, positionHeight: number,
expandTop: number = INTERSECTION_EXPAND_TOP,
expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
) { ) {
const topWindow = timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop; const headerHeight = timelineManager.headerHeight;
const bottomWindow = timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom; return calculateViewportProximity(
positionTop,
const positionBottom = positionTop + positionHeight; positionTop + positionHeight,
timelineManager.visibleWindow.top - headerHeight,
return isIntersecting(positionTop, positionBottom, topWindow, bottomWindow); timelineManager.visibleWindow.bottom + headerHeight,
);
} }
@@ -17,6 +17,11 @@ import {
import { t } from 'svelte-i18n'; import { t } from 'svelte-i18n';
import { get } from 'svelte/store'; import { get } from 'svelte/store';
import {
ViewportProximity,
isInOrNearViewport as isInOrNearViewportUtil,
isInViewport as isInViewportUtil,
} from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
import { SvelteSet } from 'svelte/reactivity'; import { SvelteSet } from 'svelte/reactivity';
import { DayGroup } from './day-group.svelte'; import { DayGroup } from './day-group.svelte';
import { GroupInsertionCache } from './group-insertion-cache.svelte'; import { GroupInsertionCache } from './group-insertion-cache.svelte';
@@ -25,8 +30,7 @@ import type { AssetDescriptor, Direction, MoveAsset, TimelineAsset } from './typ
import { ViewerAsset } from './viewer-asset.svelte'; import { ViewerAsset } from './viewer-asset.svelte';
export class MonthGroup { export class MonthGroup {
#intersecting: boolean = $state(false); #viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport);
actuallyIntersecting: boolean = $state(false);
isLoaded: boolean = $state(false); isLoaded: boolean = $state(false);
dayGroups: DayGroup[] = $state([]); dayGroups: DayGroup[] = $state([]);
readonly timelineManager: TimelineManager; readonly timelineManager: TimelineManager;
@@ -78,21 +82,25 @@ export class MonthGroup {
} }
} }
set intersecting(newValue: boolean) { set viewportProximity(newValue: ViewportProximity) {
const old = this.#intersecting; const old = this.#viewportProximity;
if (old === newValue) { if (old === newValue) {
return; return;
} }
this.#intersecting = newValue; this.#viewportProximity = newValue;
if (newValue) { if (isInOrNearViewportUtil(newValue)) {
void this.timelineManager.loadMonthGroup(this.yearMonth); void this.timelineManager.loadMonthGroup(this.yearMonth);
} else { } else {
this.cancel(); this.cancel();
} }
} }
get intersecting() { get isInOrNearViewport() {
return this.#intersecting; return isInOrNearViewportUtil(this.#viewportProximity);
}
get isInViewport() {
return isInViewportUtil(this.#viewportProximity);
} }
get lastDayGroup() { get lastDayGroup() {
@@ -2,7 +2,7 @@ import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/Virtual
import { authManager } from '$lib/managers/auth-manager.svelte'; import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.svelte'; import { eventManager } from '$lib/managers/event-manager.svelte';
import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte'; import { GroupInsertionCache } from '$lib/managers/timeline-manager/group-insertion-cache.svelte';
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte'; import { updateMonthGroupViewportProximity } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte'; import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte'; import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
import { import {
@@ -91,7 +91,7 @@ export class TimelineManager extends VirtualScrollManager {
static #INIT_OPTIONS = {}; static #INIT_OPTIONS = {};
#websocketSupport: WebsocketSupport | undefined; #websocketSupport: WebsocketSupport | undefined;
#options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS; #options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS;
#updatingIntersections = false; #updatingViewportProximities = false;
#scrollableElement: HTMLElement | undefined = $state(); #scrollableElement: HTMLElement | undefined = $state();
#showAssetOwners = new PersistedLocalStorage<boolean>('album-show-asset-owners', false); #showAssetOwners = new PersistedLocalStorage<boolean>('album-show-asset-owners', false);
#unsubscribes: Array<() => void> = []; #unsubscribes: Array<() => void> = [];
@@ -198,17 +198,21 @@ export class TimelineManager extends VirtualScrollManager {
return clamp((this.visibleWindow.top - month.top) / month.height, 0, 1); return clamp((this.visibleWindow.top - month.top) / month.height, 0, 1);
} }
override updateIntersections() { override updateViewportProximities() {
if (this.#updatingIntersections || !this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) { if (
this.#updatingViewportProximities ||
!this.isInitialized ||
this.visibleWindow.bottom === this.visibleWindow.top
) {
return; return;
} }
this.#updatingIntersections = true; this.#updatingViewportProximities = true;
for (const month of this.months) { for (const month of this.months) {
updateIntersectionMonthGroup(this, month); updateMonthGroupViewportProximity(this, month);
} }
const month = this.months.find((month) => month.actuallyIntersecting); const month = this.months.find((month) => month.isInViewport);
const viewportTopRatioInMonth = this.#calculateVewportTopRatioInMonth(month); const viewportTopRatioInMonth = this.#calculateVewportTopRatioInMonth(month);
const monthBottomViewportRatio = this.#calculateMonthBottomViewportRatio(month); const monthBottomViewportRatio = this.#calculateMonthBottomViewportRatio(month);
@@ -218,7 +222,7 @@ export class TimelineManager extends VirtualScrollManager {
viewportTopRatioInMonth, viewportTopRatioInMonth,
}; };
this.#updatingIntersections = false; this.#updatingViewportProximities = false;
} }
clearDeferredLayout(month: MonthGroup) { clearDeferredLayout(month: MonthGroup) {
@@ -317,7 +321,7 @@ export class TimelineManager extends VirtualScrollManager {
for (const month of this.months) { for (const month of this.months) {
updateGeometry(this, month, { invalidateHeight: changedWidth }); updateGeometry(this, month, { invalidateHeight: changedWidth });
} }
this.updateIntersections(); this.updateViewportProximities();
if (changedWidth) { if (changedWidth) {
this.#createScrubberMonths(); this.#createScrubberMonths();
} }
@@ -353,7 +357,7 @@ export class TimelineManager extends VirtualScrollManager {
}, cancelable); }, cancelable);
if (executionStatus === 'LOADED') { if (executionStatus === 'LOADED') {
updateGeometry(this, monthGroup, { invalidateHeight: false }); updateGeometry(this, monthGroup, { invalidateHeight: false });
this.updateIntersections(); this.updateViewportProximities();
} }
} }
@@ -538,7 +542,7 @@ export class TimelineManager extends VirtualScrollManager {
updateGeometry(this, month, { invalidateHeight: true }); updateGeometry(this, month, { invalidateHeight: true });
} }
if (changedGeometry) { if (changedGeometry) {
this.updateIntersections(); this.updateViewportProximities();
} }
return { updated, notUpdated, changedGeometry }; return { updated, notUpdated, changedGeometry };
} }
@@ -547,7 +551,7 @@ export class TimelineManager extends VirtualScrollManager {
for (const month of this.months) { for (const month of this.months) {
updateGeometry(this, month, { invalidateHeight: true }); updateGeometry(this, month, { invalidateHeight: true });
} }
this.updateIntersections(); this.updateViewportProximities();
} }
getFirstAsset(): TimelineAsset | undefined { getFirstAsset(): TimelineAsset | undefined {
@@ -626,6 +630,6 @@ export class TimelineManager extends VirtualScrollManager {
month.sortDayGroups(); month.sortDayGroups();
updateGeometry(this, month, { invalidateHeight: true }); updateGeometry(this, month, { invalidateHeight: true });
} }
this.updateIntersections(); this.updateViewportProximities();
} }
} }
@@ -2,3 +2,7 @@ import type { TimelineAsset } from './types';
export const assetSnapshot = (asset: TimelineAsset): TimelineAsset => $state.snapshot(asset); export const assetSnapshot = (asset: TimelineAsset): TimelineAsset => $state.snapshot(asset);
export const assetsSnapshot = (assets: TimelineAsset[]) => assets.map((asset) => $state.snapshot(asset)); export const assetsSnapshot = (assets: TimelineAsset[]) => assets.map((asset) => $state.snapshot(asset));
export function filterIsInOrNearViewport<T extends { isInOrNearViewport: boolean }>(items: T[]) {
return items.filter(({ isInOrNearViewport }) => isInOrNearViewport);
}
@@ -1,23 +1,31 @@
import type { CommonPosition } from '$lib/utils/layout-utils'; import type { CommonPosition } from '$lib/utils/layout-utils';
import type { DayGroup } from './day-group.svelte'; import type { DayGroup } from './day-group.svelte';
import { calculateViewerAssetIntersecting } from './internal/intersection-support.svelte'; import {
ViewportProximity,
calculateViewerAssetViewportProximity,
isInOrNearViewport,
} from './internal/intersection-support.svelte';
import type { TimelineAsset } from './types'; import type { TimelineAsset } from './types';
export class ViewerAsset { export class ViewerAsset {
readonly #group: DayGroup; readonly #group: DayGroup;
intersecting = $derived.by(() => { #viewportProximity = $derived.by(() => {
if (!this.position) { if (!this.position) {
return false; return ViewportProximity.FarFromViewport;
} }
const store = this.#group.monthGroup.timelineManager; const store = this.#group.monthGroup.timelineManager;
const positionTop = this.#group.absoluteDayGroupTop + this.position.top; const positionTop = this.#group.absoluteDayGroupTop + this.position.top;
return calculateViewerAssetIntersecting(store, positionTop, this.position.height); return calculateViewerAssetViewportProximity(store, positionTop, this.position.height);
}); });
get isInOrNearViewport() {
return isInOrNearViewport(this.#viewportProximity);
}
position: CommonPosition | undefined = $state.raw(); position: CommonPosition | undefined = $state.raw();
asset: TimelineAsset = <TimelineAsset>$state(); asset: TimelineAsset = <TimelineAsset>$state();
id: string = $derived(this.asset.id); id: string = $derived(this.asset.id);