mirror of
https://github.com/immich-app/immich.git
synced 2026-03-22 05:39:38 +03:00
refactor(web): rename intersecting/actuallyIntersecting to nearby/intersecting
Change-Id: Id6f63247441fe290e7732e489f73e8c16a6a6964
This commit is contained in:
@@ -34,7 +34,7 @@
|
|||||||
thumbnailSize?: number;
|
thumbnailSize?: number;
|
||||||
thumbnailWidth?: number;
|
thumbnailWidth?: number;
|
||||||
thumbnailHeight?: number;
|
thumbnailHeight?: number;
|
||||||
actuallyIntersecting?: boolean;
|
intersecting?: boolean;
|
||||||
selected?: boolean;
|
selected?: boolean;
|
||||||
selectionCandidate?: boolean;
|
selectionCandidate?: boolean;
|
||||||
disabled?: boolean;
|
disabled?: boolean;
|
||||||
@@ -57,7 +57,7 @@
|
|||||||
thumbnailSize = undefined,
|
thumbnailSize = undefined,
|
||||||
thumbnailWidth = undefined,
|
thumbnailWidth = undefined,
|
||||||
thumbnailHeight = undefined,
|
thumbnailHeight = undefined,
|
||||||
actuallyIntersecting = true,
|
intersecting = true,
|
||||||
selected = false,
|
selected = false,
|
||||||
selectionCandidate = false,
|
selectionCandidate = false,
|
||||||
disabled = false,
|
disabled = false,
|
||||||
@@ -89,7 +89,7 @@
|
|||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (loaded && !loadedEffectRan) {
|
if (loaded && !loadedEffectRan) {
|
||||||
loadedEffectRan = true;
|
loadedEffectRan = true;
|
||||||
if (!actuallyIntersecting) {
|
if (!intersecting) {
|
||||||
skipFade = true;
|
skipFade = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,6 @@
|
|||||||
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
|
||||||
import { showDeleteModal } from '$lib/stores/preferences.store';
|
import { showDeleteModal } from '$lib/stores/preferences.store';
|
||||||
import { handlePromiseError } from '$lib/utils';
|
import { handlePromiseError } from '$lib/utils';
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
|
||||||
import { deleteAssets } from '$lib/utils/actions';
|
import { deleteAssets } from '$lib/utils/actions';
|
||||||
import {
|
import {
|
||||||
archiveAssets,
|
archiveAssets,
|
||||||
@@ -28,6 +27,7 @@
|
|||||||
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
import { getJustifiedLayoutFromAssets } from '$lib/utils/layout-utils';
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util';
|
import { isTimelineAsset, toTimelineAsset } from '$lib/utils/timeline-util';
|
||||||
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import { AssetVisibility, type AssetResponseDto } from '@immich/sdk';
|
import { AssetVisibility, type AssetResponseDto } from '@immich/sdk';
|
||||||
import { modalManager } from '@immich/ui';
|
import { modalManager } from '@immich/ui';
|
||||||
import { debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
@@ -82,14 +82,14 @@
|
|||||||
return `top: ${geo.getTop(i)}px; left: ${geo.getLeft(i)}px; width: ${geo.getWidth(i)}px; height: ${geo.getHeight(i)}px;`;
|
return `top: ${geo.getTop(i)}px; left: ${geo.getLeft(i)}px; width: ${geo.getWidth(i)}px; height: ${geo.getHeight(i)}px;`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isIntersecting = (i: number) => {
|
const isRenderable = (i: number) => {
|
||||||
const geo = geometry;
|
const geo = geometry;
|
||||||
const window = slidingWindow;
|
const window = slidingWindow;
|
||||||
const top = geo.getTop(i);
|
const top = geo.getTop(i);
|
||||||
return top + pageHeaderOffset < window.bottom && top + geo.getHeight(i) > window.top;
|
return top + pageHeaderOffset < window.bottom && top + geo.getHeight(i) > window.top;
|
||||||
};
|
};
|
||||||
|
|
||||||
const isActuallyIntersecting = (i: number) => {
|
const isIntersecting = (i: number) => {
|
||||||
const geo = geometry;
|
const geo = geometry;
|
||||||
const top = geo.getTop(i) + pageHeaderOffset;
|
const top = geo.getTop(i) + pageHeaderOffset;
|
||||||
const bottom = top + geo.getHeight(i);
|
const bottom = top + geo.getHeight(i);
|
||||||
@@ -387,7 +387,7 @@
|
|||||||
style:width={geometry.containerWidth + 'px'}
|
style:width={geometry.containerWidth + 'px'}
|
||||||
>
|
>
|
||||||
{#each assets as asset, i (asset.id + '-' + i)}
|
{#each assets as asset, i (asset.id + '-' + i)}
|
||||||
{#if isIntersecting(i)}
|
{#if isRenderable(i)}
|
||||||
{@const currentAsset = toTimelineAsset(asset)}
|
{@const currentAsset = toTimelineAsset(asset)}
|
||||||
<div class="absolute" style:overflow="clip" style={getStyle(i)}>
|
<div class="absolute" style:overflow="clip" style={getStyle(i)}>
|
||||||
<Thumbnail
|
<Thumbnail
|
||||||
@@ -405,7 +405,7 @@
|
|||||||
asset={currentAsset}
|
asset={currentAsset}
|
||||||
selected={assetInteraction.hasSelectedAsset(currentAsset.id)}
|
selected={assetInteraction.hasSelectedAsset(currentAsset.id)}
|
||||||
selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)}
|
selectionCandidate={assetInteraction.hasSelectionCandidate(currentAsset.id)}
|
||||||
actuallyIntersecting={isActuallyIntersecting(i)}
|
intersecting={isIntersecting(i)}
|
||||||
thumbnailWidth={geometry.getWidth(i)}
|
thumbnailWidth={geometry.getWidth(i)}
|
||||||
thumbnailHeight={geometry.getHeight(i)}
|
thumbnailHeight={geometry.getHeight(i)}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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 { filterRenderable } 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';
|
||||||
@@ -20,7 +21,7 @@
|
|||||||
{
|
{
|
||||||
asset: TimelineAsset;
|
asset: TimelineAsset;
|
||||||
position: CommonPosition;
|
position: CommonPosition;
|
||||||
actuallyIntersecting: boolean;
|
intersecting: boolean;
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
>;
|
>;
|
||||||
@@ -31,18 +32,14 @@
|
|||||||
|
|
||||||
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 filterRenderable(viewerAssets) as viewerAsset (viewerAsset.id)}
|
||||||
{@const position = viewerAsset.position!}
|
{@const position = viewerAsset.position!}
|
||||||
{@const asset = viewerAsset.asset!}
|
{@const asset = viewerAsset.asset!}
|
||||||
{@const actuallyIntersecting = viewerAsset.actuallyIntersecting!}
|
{@const intersecting = viewerAsset.intersecting!}
|
||||||
|
|
||||||
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
|
<!-- note: don't remove data-asset-id - its used by web e2e tests -->
|
||||||
<div
|
<div
|
||||||
@@ -55,7 +52,7 @@
|
|||||||
out:scale|global={{ start: 0.1, duration: scaleDuration }}
|
out:scale|global={{ start: 0.1, duration: scaleDuration }}
|
||||||
animate:flip={{ duration: transitionDuration }}
|
animate:flip={{ duration: transitionDuration }}
|
||||||
>
|
>
|
||||||
{@render thumbnail({ asset, position, actuallyIntersecting })}
|
{@render thumbnail({ asset, position, intersecting })}
|
||||||
{@render customThumbnailLayout?.(asset)}
|
{@render customThumbnailLayout?.(asset)}
|
||||||
</div>
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
|
|||||||
@@ -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, filterRenderable } 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';
|
||||||
@@ -21,7 +21,7 @@
|
|||||||
position: CommonPosition;
|
position: CommonPosition;
|
||||||
dayGroup: DayGroup;
|
dayGroup: DayGroup;
|
||||||
groupIndex: number;
|
groupIndex: number;
|
||||||
actuallyIntersecting: boolean;
|
intersecting: boolean;
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
>;
|
>;
|
||||||
@@ -47,10 +47,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({
|
||||||
@@ -62,7 +58,7 @@
|
|||||||
};
|
};
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{#each filterIntersecting(monthGroup.dayGroups) as dayGroup, groupIndex (dayGroup.day)}
|
{#each filterRenderable(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
|
||||||
@@ -109,8 +105,8 @@
|
|||||||
width={dayGroup.width}
|
width={dayGroup.width}
|
||||||
{customThumbnailLayout}
|
{customThumbnailLayout}
|
||||||
>
|
>
|
||||||
{#snippet thumbnail({ asset, position, actuallyIntersecting })}
|
{#snippet thumbnail({ asset, position, intersecting })}
|
||||||
{@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex, actuallyIntersecting })}
|
{@render thumbnailWithGroup({ asset, position, dayGroup, groupIndex, intersecting })}
|
||||||
{/snippet}
|
{/snippet}
|
||||||
</AssetLayout>
|
</AssetLayout>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -645,7 +645,7 @@
|
|||||||
</section>
|
</section>
|
||||||
|
|
||||||
{#each timelineManager.months as monthGroup (monthGroup.viewId)}
|
{#each timelineManager.months as monthGroup (monthGroup.viewId)}
|
||||||
{@const display = monthGroup.intersecting}
|
{@const renderable = monthGroup.renderable}
|
||||||
{@const absoluteHeight = monthGroup.top}
|
{@const absoluteHeight = monthGroup.top}
|
||||||
|
|
||||||
{#if !monthGroup.isLoaded}
|
{#if !monthGroup.isLoaded}
|
||||||
@@ -657,7 +657,7 @@
|
|||||||
>
|
>
|
||||||
<Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} />
|
<Skeleton {invisible} height={monthGroup.height} title={monthGroup.monthGroupTitle} />
|
||||||
</div>
|
</div>
|
||||||
{:else if display}
|
{:else if renderable}
|
||||||
<div
|
<div
|
||||||
class="month-group"
|
class="month-group"
|
||||||
style:height={monthGroup.height + 'px'}
|
style:height={monthGroup.height + 'px'}
|
||||||
@@ -673,7 +673,7 @@
|
|||||||
manager={timelineManager}
|
manager={timelineManager}
|
||||||
onDayGroupSelect={handleGroupSelect}
|
onDayGroupSelect={handleGroupSelect}
|
||||||
>
|
>
|
||||||
{#snippet thumbnail({ asset, position, dayGroup, groupIndex, actuallyIntersecting })}
|
{#snippet thumbnail({ asset, position, dayGroup, groupIndex, intersecting })}
|
||||||
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
{@const isAssetSelectionCandidate = assetInteraction.hasSelectionCandidate(asset.id)}
|
||||||
{@const isAssetSelected =
|
{@const isAssetSelected =
|
||||||
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
|
assetInteraction.hasSelectedAsset(asset.id) || timelineManager.albumAssets.has(asset.id)}
|
||||||
@@ -684,7 +684,7 @@
|
|||||||
{asset}
|
{asset}
|
||||||
{albumUsers}
|
{albumUsers}
|
||||||
{groupIndex}
|
{groupIndex}
|
||||||
{actuallyIntersecting}
|
{intersecting}
|
||||||
onClick={(asset) => {
|
onClick={(asset) => {
|
||||||
if (typeof onThumbnailClick === 'function') {
|
if (typeof onThumbnailClick === 'function') {
|
||||||
onThumbnailClick(asset, timelineManager, dayGroup, _onClick);
|
onThumbnailClick(asset, timelineManager, dayGroup, _onClick);
|
||||||
|
|||||||
@@ -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));
|
renderable = $derived.by(() => this.viewerAssets.some((viewAsset) => viewAsset.renderable));
|
||||||
|
|
||||||
#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.renderable && !this.monthGroup.timelineManager.isScrollingOnLoad) {
|
||||||
this.#deferredLayout = true;
|
this.#deferredLayout = true;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,12 @@
|
|||||||
import { describe, expect, it } from 'vitest';
|
import { describe, expect, it } from 'vitest';
|
||||||
import type { TimelineManager } from '../timeline-manager.svelte';
|
import type { TimelineManager } from '../timeline-manager.svelte';
|
||||||
import { Intersection, calculateViewerAssetIntersecting, isIntersecting } from './intersection-support.svelte';
|
import {
|
||||||
|
IntersectionFlags,
|
||||||
|
calculateViewerAssetIntersecting,
|
||||||
|
isIntersecting,
|
||||||
|
isRenderable,
|
||||||
|
isVisible,
|
||||||
|
} from './intersection-support.svelte';
|
||||||
|
|
||||||
function createMockTimelineManager(windowTop: number, windowBottom: number, headerHeight: number = 0): TimelineManager {
|
function createMockTimelineManager(windowTop: number, windowBottom: number, headerHeight: number = 0): TimelineManager {
|
||||||
return {
|
return {
|
||||||
@@ -51,45 +57,87 @@ describe('calculateViewerAssetIntersecting', () => {
|
|||||||
// viewport 0-1000, no header, default expand margins 500/500
|
// viewport 0-1000, no header, default expand margins 500/500
|
||||||
const manager = createMockTimelineManager(0, 1000);
|
const manager = createMockTimelineManager(0, 1000);
|
||||||
|
|
||||||
it('should return ACTUAL when asset is within viewport', () => {
|
it('should return VISIBLE when asset is within viewport', () => {
|
||||||
expect(calculateViewerAssetIntersecting(manager, 100, 50)).toBe(Intersection.ACTUAL);
|
expect(calculateViewerAssetIntersecting(manager, 100, 50)).toBe(IntersectionFlags.VISIBLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return ACTUAL when asset is at viewport top edge', () => {
|
it('should return VISIBLE when asset is at viewport top edge', () => {
|
||||||
expect(calculateViewerAssetIntersecting(manager, 0, 50)).toBe(Intersection.ACTUAL);
|
expect(calculateViewerAssetIntersecting(manager, 0, 50)).toBe(IntersectionFlags.VISIBLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return ACTUAL when asset partially overlaps viewport bottom', () => {
|
it('should return VISIBLE when asset partially overlaps viewport bottom', () => {
|
||||||
expect(calculateViewerAssetIntersecting(manager, 980, 50)).toBe(Intersection.ACTUAL);
|
expect(calculateViewerAssetIntersecting(manager, 980, 50)).toBe(IntersectionFlags.VISIBLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return PRE when asset is just above viewport within expand margin', () => {
|
it('should return NEARBY when asset is just above viewport within expand margin', () => {
|
||||||
expect(calculateViewerAssetIntersecting(manager, -200, 50)).toBe(Intersection.PRE);
|
expect(calculateViewerAssetIntersecting(manager, -200, 50)).toBe(IntersectionFlags.NEARBY);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return PRE when asset is just below viewport within expand margin', () => {
|
it('should return NEARBY when asset is just below viewport within expand margin', () => {
|
||||||
expect(calculateViewerAssetIntersecting(manager, 1200, 50)).toBe(Intersection.PRE);
|
expect(calculateViewerAssetIntersecting(manager, 1200, 50)).toBe(IntersectionFlags.NEARBY);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return NONE when asset is far above viewport', () => {
|
it('should return NONE when asset is far above viewport', () => {
|
||||||
expect(calculateViewerAssetIntersecting(manager, -1000, 50)).toBe(Intersection.NONE);
|
expect(calculateViewerAssetIntersecting(manager, -1000, 50)).toBe(IntersectionFlags.NONE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return NONE when asset is far below viewport', () => {
|
it('should return NONE when asset is far below viewport', () => {
|
||||||
expect(calculateViewerAssetIntersecting(manager, 2000, 50)).toBe(Intersection.NONE);
|
expect(calculateViewerAssetIntersecting(manager, 2000, 50)).toBe(IntersectionFlags.NONE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should account for header height in viewport bounds', () => {
|
it('should account for header height in viewport bounds', () => {
|
||||||
const managerWithHeader = createMockTimelineManager(100, 500, 50);
|
const managerWithHeader = createMockTimelineManager(100, 500, 50);
|
||||||
// viewport effectively becomes (100-50)=50 to (500+50)=550
|
// viewport effectively becomes (100-50)=50 to (500+50)=550
|
||||||
// asset at 40-90 overlaps the effective viewport
|
// asset at 40-90 overlaps the effective viewport
|
||||||
expect(calculateViewerAssetIntersecting(managerWithHeader, 40, 50)).toBe(Intersection.ACTUAL);
|
expect(calculateViewerAssetIntersecting(managerWithHeader, 40, 50)).toBe(IntersectionFlags.VISIBLE);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return PRE not ACTUAL for asset outside viewport but within header-adjusted expand', () => {
|
it('should return NEARBY not VISIBLE for asset outside viewport but within header-adjusted expand', () => {
|
||||||
const managerWithHeader = createMockTimelineManager(100, 500, 50);
|
const managerWithHeader = createMockTimelineManager(100, 500, 50);
|
||||||
// effective viewport: 50-550, expand: 500 each way -> -450 to 1050
|
// effective viewport: 50-550, expand: 500 each way -> -450 to 1050
|
||||||
// asset at -400 to -350 is outside viewport but within expand
|
// asset at -400 to -350 is outside viewport but within expand
|
||||||
expect(calculateViewerAssetIntersecting(managerWithHeader, -400, 50)).toBe(Intersection.PRE);
|
expect(calculateViewerAssetIntersecting(managerWithHeader, -400, 50)).toBe(IntersectionFlags.NEARBY);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('Intersection flags', () => {
|
||||||
|
it('RENDERABLE should be NEARBY | VISIBLE', () => {
|
||||||
|
expect(IntersectionFlags.RENDERABLE).toBe(IntersectionFlags.NEARBY | IntersectionFlags.VISIBLE);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isVisible', () => {
|
||||||
|
it('should return true for VISIBLE', () => {
|
||||||
|
expect(isVisible(IntersectionFlags.VISIBLE)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for RENDERABLE', () => {
|
||||||
|
expect(isVisible(IntersectionFlags.RENDERABLE)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for NEARBY', () => {
|
||||||
|
expect(isVisible(IntersectionFlags.NEARBY)).toBe(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for NONE', () => {
|
||||||
|
expect(isVisible(IntersectionFlags.NONE)).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('isRenderable', () => {
|
||||||
|
it('should return true for VISIBLE', () => {
|
||||||
|
expect(isRenderable(IntersectionFlags.VISIBLE)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for NEARBY', () => {
|
||||||
|
expect(isRenderable(IntersectionFlags.NEARBY)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return true for RENDERABLE', () => {
|
||||||
|
expect(isRenderable(IntersectionFlags.RENDERABLE)).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false for NONE', () => {
|
||||||
|
expect(isRenderable(IntersectionFlags.NONE)).toBe(false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
/* eslint-disable unicorn/prefer-math-trunc */
|
||||||
import { TUNABLES } from '$lib/utils/tunables';
|
import { TUNABLES } from '$lib/utils/tunables';
|
||||||
import type { MonthGroup } from '../month-group.svelte';
|
import type { MonthGroup } from '../month-group.svelte';
|
||||||
import { TimelineManager } from '../timeline-manager.svelte';
|
import { TimelineManager } from '../timeline-manager.svelte';
|
||||||
@@ -7,75 +8,72 @@ const {
|
|||||||
} = TUNABLES;
|
} = TUNABLES;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* General function to check if a rectangular region intersects with a window.
|
* General function to check if a rectangular region intersects with another regtangular region.
|
||||||
*/
|
*/
|
||||||
export function isIntersecting(regionTop: number, regionBottom: number, windowTop: number, windowBottom: number) {
|
export function isIntersecting(regionTop: number, regionBottom: number, otherTop: number, otherBottom: number) {
|
||||||
return (
|
return (
|
||||||
(regionTop >= windowTop && regionTop < windowBottom) ||
|
(regionTop >= otherTop && regionTop < otherBottom) ||
|
||||||
(regionBottom >= windowTop && regionBottom < windowBottom) ||
|
(regionBottom >= otherTop && regionBottom < otherBottom) ||
|
||||||
(regionTop < windowTop && regionBottom >= windowBottom)
|
(regionTop < otherTop && regionBottom >= otherBottom)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function updateIntersectionMonthGroup(timelineManager: TimelineManager, month: MonthGroup) {
|
const NEARBY = 1 << 0;
|
||||||
const monthGroupTop = month.top;
|
const VISIBLE = 1 << 1;
|
||||||
const monthGroupBottom = monthGroupTop + month.height;
|
|
||||||
const windowTop = timelineManager.visibleWindow.top;
|
|
||||||
const windowBottom = timelineManager.visibleWindow.bottom;
|
|
||||||
|
|
||||||
const actuallyIntersecting = isIntersecting(monthGroupTop, monthGroupBottom, windowTop, windowBottom);
|
export const IntersectionFlags = {
|
||||||
|
NONE: 0,
|
||||||
|
NEARBY,
|
||||||
|
VISIBLE,
|
||||||
|
RENDERABLE: NEARBY | VISIBLE,
|
||||||
|
} as const;
|
||||||
|
|
||||||
let intersecting = actuallyIntersecting;
|
export type IntersectionFlag = (typeof IntersectionFlags)[keyof typeof IntersectionFlags];
|
||||||
if (!actuallyIntersecting) {
|
|
||||||
intersecting = isIntersecting(
|
export function isVisible(flag: number): boolean {
|
||||||
monthGroupTop,
|
return (flag & IntersectionFlags.VISIBLE) !== 0;
|
||||||
monthGroupBottom,
|
}
|
||||||
windowTop - INTERSECTION_EXPAND_TOP,
|
|
||||||
windowBottom + INTERSECTION_EXPAND_BOTTOM,
|
export function isRenderable(flag: number): boolean {
|
||||||
);
|
return (flag & IntersectionFlags.RENDERABLE) !== 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateIntersection(regionTop: number, regionBottom: number, windowTop: number, windowBottom: number) {
|
||||||
|
if (regionBottom < windowTop - INTERSECTION_EXPAND_TOP || regionTop >= windowBottom + INTERSECTION_EXPAND_BOTTOM) {
|
||||||
|
return IntersectionFlags.NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
month.intersecting = intersecting;
|
if (regionBottom < windowTop || regionTop >= windowBottom) {
|
||||||
month.actuallyIntersecting = actuallyIntersecting;
|
return IntersectionFlags.NEARBY;
|
||||||
if (intersecting) {
|
}
|
||||||
|
|
||||||
|
return IntersectionFlags.VISIBLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function updateIntersectionMonthGroup(timelineManager: TimelineManager, month: MonthGroup) {
|
||||||
|
const intersection = calculateIntersection(
|
||||||
|
month.top,
|
||||||
|
month.top + month.height,
|
||||||
|
timelineManager.visibleWindow.top,
|
||||||
|
timelineManager.visibleWindow.bottom,
|
||||||
|
);
|
||||||
|
|
||||||
|
month.intersection = intersection;
|
||||||
|
if (isRenderable(intersection)) {
|
||||||
timelineManager.clearDeferredLayout(month);
|
timelineManager.clearDeferredLayout(month);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Bit flags for intersection state
|
|
||||||
export const Intersection = {
|
|
||||||
NONE: 0,
|
|
||||||
PRE: 1,
|
|
||||||
ACTUAL: 3, // includes PRE (both bits set)
|
|
||||||
} as const;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns a numeric flag: NONE (0), PRE (1, within expanded margin only), or ACTUAL (3, truly visible).
|
|
||||||
*/
|
|
||||||
export function calculateViewerAssetIntersecting(
|
export function calculateViewerAssetIntersecting(
|
||||||
timelineManager: TimelineManager,
|
timelineManager: TimelineManager,
|
||||||
positionTop: number,
|
positionTop: number,
|
||||||
positionHeight: number,
|
positionHeight: number,
|
||||||
) {
|
) {
|
||||||
const positionBottom = positionTop + positionHeight;
|
|
||||||
const headerHeight = timelineManager.headerHeight;
|
const headerHeight = timelineManager.headerHeight;
|
||||||
const windowTop = timelineManager.visibleWindow.top - headerHeight;
|
return calculateIntersection(
|
||||||
const windowBottom = timelineManager.visibleWindow.bottom + headerHeight;
|
positionTop,
|
||||||
|
positionTop + positionHeight,
|
||||||
if (isIntersecting(positionTop, positionBottom, windowTop, windowBottom)) {
|
timelineManager.visibleWindow.top - headerHeight,
|
||||||
return Intersection.ACTUAL;
|
timelineManager.visibleWindow.bottom + headerHeight,
|
||||||
}
|
);
|
||||||
|
|
||||||
if (
|
|
||||||
isIntersecting(
|
|
||||||
positionTop,
|
|
||||||
positionBottom,
|
|
||||||
windowTop - INTERSECTION_EXPAND_TOP,
|
|
||||||
windowBottom + INTERSECTION_EXPAND_BOTTOM,
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
return Intersection.PRE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Intersection.NONE;
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,6 +17,12 @@ import {
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
|
import {
|
||||||
|
IntersectionFlags,
|
||||||
|
isRenderable,
|
||||||
|
isVisible,
|
||||||
|
type IntersectionFlag,
|
||||||
|
} 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 +31,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);
|
#intersection: IntersectionFlag = $state(IntersectionFlags.NONE);
|
||||||
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 +83,25 @@ export class MonthGroup {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
set intersecting(newValue: boolean) {
|
set intersection(newValue: IntersectionFlag) {
|
||||||
const old = this.#intersecting;
|
const old = this.#intersection;
|
||||||
if (old === newValue) {
|
if (old === newValue) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.#intersecting = newValue;
|
this.#intersection = newValue;
|
||||||
if (newValue) {
|
if (isRenderable(newValue)) {
|
||||||
void this.timelineManager.loadMonthGroup(this.yearMonth);
|
void this.timelineManager.loadMonthGroup(this.yearMonth);
|
||||||
} else {
|
} else {
|
||||||
this.cancel();
|
this.cancel();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
get renderable() {
|
||||||
|
return isRenderable(this.#intersection);
|
||||||
|
}
|
||||||
|
|
||||||
get intersecting() {
|
get intersecting() {
|
||||||
return this.#intersecting;
|
return isVisible(this.#intersection);
|
||||||
}
|
}
|
||||||
|
|
||||||
get lastDayGroup() {
|
get lastDayGroup() {
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ export class TimelineManager extends VirtualScrollManager {
|
|||||||
updateIntersectionMonthGroup(this, month);
|
updateIntersectionMonthGroup(this, month);
|
||||||
}
|
}
|
||||||
|
|
||||||
const month = this.months.find((month) => month.actuallyIntersecting);
|
const month = this.months.find((month) => month.intersecting);
|
||||||
const viewportTopRatioInMonth = this.#calculateVewportTopRatioInMonth(month);
|
const viewportTopRatioInMonth = this.#calculateVewportTopRatioInMonth(month);
|
||||||
const monthBottomViewportRatio = this.#calculateMonthBottomViewportRatio(month);
|
const monthBottomViewportRatio = this.#calculateMonthBottomViewportRatio(month);
|
||||||
|
|
||||||
|
|||||||
@@ -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 filterRenderable<T extends { renderable: boolean }>(items: T[]) {
|
||||||
|
return items.filter(({ renderable }) => renderable);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
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 { Intersection, calculateViewerAssetIntersecting } from './internal/intersection-support.svelte';
|
import {
|
||||||
|
IntersectionFlags,
|
||||||
|
calculateViewerAssetIntersecting,
|
||||||
|
isRenderable,
|
||||||
|
isVisible,
|
||||||
|
} from './internal/intersection-support.svelte';
|
||||||
import type { TimelineAsset } from './types';
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
export class ViewerAsset {
|
export class ViewerAsset {
|
||||||
@@ -9,7 +14,7 @@ export class ViewerAsset {
|
|||||||
|
|
||||||
#intersection = $derived.by(() => {
|
#intersection = $derived.by(() => {
|
||||||
if (!this.position) {
|
if (!this.position) {
|
||||||
return Intersection.NONE;
|
return IntersectionFlags.NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
const store = this.#group.monthGroup.timelineManager;
|
const store = this.#group.monthGroup.timelineManager;
|
||||||
@@ -18,12 +23,12 @@ export class ViewerAsset {
|
|||||||
return calculateViewerAssetIntersecting(store, positionTop, this.position.height);
|
return calculateViewerAssetIntersecting(store, positionTop, this.position.height);
|
||||||
});
|
});
|
||||||
|
|
||||||
get intersecting() {
|
get renderable() {
|
||||||
return this.#intersection !== Intersection.NONE;
|
return isRenderable(this.#intersection);
|
||||||
}
|
}
|
||||||
|
|
||||||
get actuallyIntersecting() {
|
get intersecting() {
|
||||||
return this.#intersection === Intersection.ACTUAL;
|
return isVisible(this.#intersection);
|
||||||
}
|
}
|
||||||
|
|
||||||
position: CommonPosition | undefined = $state.raw();
|
position: CommonPosition | undefined = $state.raw();
|
||||||
|
|||||||
Reference in New Issue
Block a user