diff --git a/web/src/lib/components/timeline/AssetLayout.svelte b/web/src/lib/components/timeline/AssetLayout.svelte
index 8b06d9b72b..4ecf71f517 100644
--- a/web/src/lib/components/timeline/AssetLayout.svelte
+++ b/web/src/lib/components/timeline/AssetLayout.svelte
@@ -1,5 +1,6 @@
- {#each filterIntersecting(viewerAssets) as viewerAsset (viewerAsset.id)}
+ {#each filterIsInOrNearViewport(viewerAssets) as viewerAsset (viewerAsset.id)}
{@const position = viewerAsset.position!}
{@const asset = viewerAsset.asset!}
diff --git a/web/src/lib/components/timeline/Month.svelte b/web/src/lib/components/timeline/Month.svelte
index 91073a0a5f..c0b20b17bb 100644
--- a/web/src/lib/components/timeline/Month.svelte
+++ b/web/src/lib/components/timeline/Month.svelte
@@ -3,7 +3,7 @@
import { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
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 { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { uploadAssetsStore } from '$lib/stores/upload';
@@ -14,7 +14,16 @@
import type { Snippet } from 'svelte';
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]>;
singleSelect: boolean;
assetInteraction: AssetInteraction;
@@ -37,10 +46,6 @@
const transitionDuration = $derived(monthGroup.timelineManager.suspendTransitions && !$isUploading ? 0 : 150);
- const filterIntersecting = (intersectables: T[]) => {
- return intersectables.filter(({ intersecting }) => intersecting);
- };
-
const getDayGroupFullDate = (dayGroup: DayGroup): string => {
const { month, year } = dayGroup.monthGroup.yearMonth;
const date = fromTimelinePlainDate({
@@ -52,7 +57,7 @@
};
-{#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)}
{#each timelineManager.months as monthGroup (monthGroup.viewId)}
- {@const display = monthGroup.intersecting}
+ {@const isInOrNearViewport = monthGroup.isInOrNearViewport}
{@const absoluteHeight = monthGroup.top}
{#if !monthGroup.isLoaded}
@@ -654,7 +654,7 @@
>
- {:else if display}
+ {:else if isInOrNearViewport}
this.viewerAssets.some((viewAsset) => viewAsset.intersecting));
+ isInOrNearViewport = $derived.by(() => this.viewerAssets.some((viewAsset) => viewAsset.isInOrNearViewport));
#top: number = $state(0);
#start: number = $state(0);
@@ -137,7 +137,7 @@ export class DayGroup {
}
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;
return;
}
diff --git a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts
index 3c6f2d8256..6fa8ab88c0 100644
--- a/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/internal/intersection-support.svelte.ts
@@ -6,68 +6,64 @@ const {
TIMELINE: { INTERSECTION_EXPAND_TOP, INTERSECTION_EXPAND_BOTTOM },
} = TUNABLES;
-export function updateIntersectionMonthGroup(timelineManager: TimelineManager, month: MonthGroup) {
- const actuallyIntersecting = calculateMonthGroupIntersecting(timelineManager, month, 0, 0);
- let preIntersecting = false;
- if (!actuallyIntersecting) {
- preIntersecting = calculateMonthGroupIntersecting(
- timelineManager,
- month,
- INTERSECTION_EXPAND_TOP,
- INTERSECTION_EXPAND_BOTTOM,
- );
+export function isIntersecting(regionTop: number, regionBottom: number, otherTop: number, otherBottom: number) {
+ return (
+ (regionTop >= otherTop && regionTop < otherBottom) ||
+ (regionBottom >= otherTop && regionBottom < otherBottom) ||
+ (regionTop < otherTop && regionBottom >= otherBottom)
+ );
+}
+
+export enum ViewportProximity {
+ 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;
}
- month.intersecting = actuallyIntersecting || preIntersecting;
- month.actuallyIntersecting = actuallyIntersecting;
- if (preIntersecting || actuallyIntersecting) {
+
+ 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);
}
}
-/**
- * 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(
+export function calculateViewerAssetViewportProximity(
timelineManager: TimelineManager,
positionTop: number,
positionHeight: number,
- expandTop: number = INTERSECTION_EXPAND_TOP,
- expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
) {
- const topWindow = timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop;
- const bottomWindow = timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom;
-
- const positionBottom = positionTop + positionHeight;
-
- return isIntersecting(positionTop, positionBottom, topWindow, bottomWindow);
+ const headerHeight = timelineManager.headerHeight;
+ return calculateViewportProximity(
+ positionTop,
+ positionTop + positionHeight,
+ timelineManager.visibleWindow.top - headerHeight,
+ timelineManager.visibleWindow.bottom + headerHeight,
+ );
}
diff --git a/web/src/lib/managers/timeline-manager/month-group.svelte.ts b/web/src/lib/managers/timeline-manager/month-group.svelte.ts
index b41deb5785..d23dc1b801 100644
--- a/web/src/lib/managers/timeline-manager/month-group.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/month-group.svelte.ts
@@ -17,6 +17,11 @@ import {
import { t } from 'svelte-i18n';
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 { DayGroup } from './day-group.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';
export class MonthGroup {
- #intersecting: boolean = $state(false);
- actuallyIntersecting: boolean = $state(false);
+ #viewportProximity: ViewportProximity = $state(ViewportProximity.FarFromViewport);
isLoaded: boolean = $state(false);
dayGroups: DayGroup[] = $state([]);
readonly timelineManager: TimelineManager;
@@ -78,21 +82,25 @@ export class MonthGroup {
}
}
- set intersecting(newValue: boolean) {
- const old = this.#intersecting;
+ set viewportProximity(newValue: ViewportProximity) {
+ const old = this.#viewportProximity;
if (old === newValue) {
return;
}
- this.#intersecting = newValue;
- if (newValue) {
+ this.#viewportProximity = newValue;
+ if (isInOrNearViewportUtil(newValue)) {
void this.timelineManager.loadMonthGroup(this.yearMonth);
} else {
this.cancel();
}
}
- get intersecting() {
- return this.#intersecting;
+ get isInOrNearViewport() {
+ return isInOrNearViewportUtil(this.#viewportProximity);
+ }
+
+ get isInViewport() {
+ return isInViewportUtil(this.#viewportProximity);
}
get lastDayGroup() {
diff --git a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
index 38c593bd00..9ab884b059 100644
--- a/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/timeline-manager.svelte.ts
@@ -2,7 +2,7 @@ import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/Virtual
import { authManager } from '$lib/managers/auth-manager.svelte';
import { eventManager } from '$lib/managers/event-manager.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 { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
import {
@@ -91,7 +91,7 @@ export class TimelineManager extends VirtualScrollManager {
static #INIT_OPTIONS = {};
#websocketSupport: WebsocketSupport | undefined;
#options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS;
- #updatingIntersections = false;
+ #updatingViewportProximities = false;
#scrollableElement: HTMLElement | undefined = $state();
#showAssetOwners = new PersistedLocalStorage('album-show-asset-owners', false);
#unsubscribes: Array<() => void> = [];
@@ -198,17 +198,21 @@ export class TimelineManager extends VirtualScrollManager {
return clamp((this.visibleWindow.top - month.top) / month.height, 0, 1);
}
- override updateIntersections() {
- if (this.#updatingIntersections || !this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
+ override updateViewportProximities() {
+ if (
+ this.#updatingViewportProximities ||
+ !this.isInitialized ||
+ this.visibleWindow.bottom === this.visibleWindow.top
+ ) {
return;
}
- this.#updatingIntersections = true;
+ this.#updatingViewportProximities = true;
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 monthBottomViewportRatio = this.#calculateMonthBottomViewportRatio(month);
@@ -218,7 +222,7 @@ export class TimelineManager extends VirtualScrollManager {
viewportTopRatioInMonth,
};
- this.#updatingIntersections = false;
+ this.#updatingViewportProximities = false;
}
clearDeferredLayout(month: MonthGroup) {
@@ -317,7 +321,7 @@ export class TimelineManager extends VirtualScrollManager {
for (const month of this.months) {
updateGeometry(this, month, { invalidateHeight: changedWidth });
}
- this.updateIntersections();
+ this.updateViewportProximities();
if (changedWidth) {
this.#createScrubberMonths();
}
@@ -353,7 +357,7 @@ export class TimelineManager extends VirtualScrollManager {
}, cancelable);
if (executionStatus === 'LOADED') {
updateGeometry(this, monthGroup, { invalidateHeight: false });
- this.updateIntersections();
+ this.updateViewportProximities();
}
}
@@ -538,7 +542,7 @@ export class TimelineManager extends VirtualScrollManager {
updateGeometry(this, month, { invalidateHeight: true });
}
if (changedGeometry) {
- this.updateIntersections();
+ this.updateViewportProximities();
}
return { updated, notUpdated, changedGeometry };
}
@@ -547,7 +551,7 @@ export class TimelineManager extends VirtualScrollManager {
for (const month of this.months) {
updateGeometry(this, month, { invalidateHeight: true });
}
- this.updateIntersections();
+ this.updateViewportProximities();
}
getFirstAsset(): TimelineAsset | undefined {
@@ -626,6 +630,6 @@ export class TimelineManager extends VirtualScrollManager {
month.sortDayGroups();
updateGeometry(this, month, { invalidateHeight: true });
}
- this.updateIntersections();
+ this.updateViewportProximities();
}
}
diff --git a/web/src/lib/managers/timeline-manager/utils.svelte.ts b/web/src/lib/managers/timeline-manager/utils.svelte.ts
index 2aba6470ee..efc94206ea 100644
--- a/web/src/lib/managers/timeline-manager/utils.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/utils.svelte.ts
@@ -2,3 +2,7 @@ import type { TimelineAsset } from './types';
export const assetSnapshot = (asset: TimelineAsset): TimelineAsset => $state.snapshot(asset);
export const assetsSnapshot = (assets: TimelineAsset[]) => assets.map((asset) => $state.snapshot(asset));
+
+export function filterIsInOrNearViewport(items: T[]) {
+ return items.filter(({ isInOrNearViewport }) => isInOrNearViewport);
+}
diff --git a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
index 161cc049f1..e0d8e1f5b5 100644
--- a/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
+++ b/web/src/lib/managers/timeline-manager/viewer-asset.svelte.ts
@@ -1,23 +1,31 @@
import type { CommonPosition } from '$lib/utils/layout-utils';
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';
export class ViewerAsset {
readonly #group: DayGroup;
- intersecting = $derived.by(() => {
+ #viewportProximity = $derived.by(() => {
if (!this.position) {
- return false;
+ return ViewportProximity.FarFromViewport;
}
const store = this.#group.monthGroup.timelineManager;
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();
asset: TimelineAsset = $state();
id: string = $derived(this.asset.id);