mirror of
https://github.com/immich-app/immich.git
synced 2026-03-29 21:02:13 -04:00
refactor(web): replace intersection booleans with enum (#27306)
Change-Id: I0c9703d5960031142ae47fef23805e0a6a6a6964
This commit is contained in:
parent
4b9ebc2cff
commit
2d950db940
@ -1,5 +1,6 @@
|
||||
<script lang="ts">
|
||||
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 { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
|
||||
import { uploadAssetsStore } from '$lib/stores/upload';
|
||||
@ -30,15 +31,11 @@
|
||||
|
||||
const transitionDuration = $derived(manager.suspendTransitions && !$isUploading ? 0 : 150);
|
||||
const scaleDuration = $derived(transitionDuration === 0 ? 0 : transitionDuration + 100);
|
||||
|
||||
const filterIntersecting = <T extends { intersecting: boolean }>(intersectables: T[]) => {
|
||||
return intersectables.filter(({ intersecting }) => intersecting);
|
||||
};
|
||||
</script>
|
||||
|
||||
<!-- Image grid -->
|
||||
<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 asset = viewerAsset.asset!}
|
||||
|
||||
|
||||
@ -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 = <T extends { intersecting: boolean }>(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 @@
|
||||
};
|
||||
</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)}
|
||||
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
||||
<section
|
||||
|
||||
@ -642,7 +642,7 @@
|
||||
</section>
|
||||
|
||||
{#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 @@
|
||||
>
|
||||
<Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} />
|
||||
</div>
|
||||
{:else if display}
|
||||
{:else if isInOrNearViewport}
|
||||
<div
|
||||
class="month-group"
|
||||
style:height={monthGroup.height + 'px'}
|
||||
|
||||
@ -138,7 +138,7 @@ export abstract class VirtualScrollManager {
|
||||
return this.viewportWidth === 0 || this.viewportHeight === 0;
|
||||
}
|
||||
|
||||
protected updateIntersections(): void {}
|
||||
protected updateViewportProximities(): void {}
|
||||
|
||||
protected updateViewportGeometry(_: boolean) {}
|
||||
|
||||
@ -156,12 +156,12 @@ export abstract class VirtualScrollManager {
|
||||
const scrollTop = this.scrollTop;
|
||||
if (this.#scrollTop !== scrollTop) {
|
||||
this.#scrollTop = scrollTop;
|
||||
this.updateIntersections();
|
||||
this.updateViewportProximities();
|
||||
}
|
||||
}
|
||||
|
||||
refreshLayout() {
|
||||
this.updateIntersections();
|
||||
this.updateViewportProximities();
|
||||
}
|
||||
|
||||
destroy(): void {}
|
||||
|
||||
@ -18,7 +18,7 @@ export class DayGroup {
|
||||
|
||||
height = $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);
|
||||
#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;
|
||||
}
|
||||
|
||||
@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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<boolean>('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();
|
||||
}
|
||||
}
|
||||
|
||||
@ -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<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 { 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 = <TimelineAsset>$state();
|
||||
id: string = $derived(this.asset.id);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user