refactor: remove scroll compensation logic from photostream components
parent
8943242b7f
commit
674def4088
|
|
@ -58,10 +58,8 @@
|
||||||
<Skeleton height={segment.height - segment.timelineManager.headerHeight} {stylePaddingHorizontalPx} />
|
<Skeleton height={segment.height - segment.timelineManager.headerHeight} {stylePaddingHorizontalPx} />
|
||||||
{/snippet}
|
{/snippet}
|
||||||
|
|
||||||
{#snippet segment({ segment, onScrollCompensationMonthInDOM })}
|
{#snippet segment({ segment })}
|
||||||
<SelectableSegment
|
<SelectableSegment
|
||||||
{segment}
|
|
||||||
{onScrollCompensationMonthInDOM}
|
|
||||||
timelineManager={searchResultsManager}
|
timelineManager={searchResultsManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
isSelectionMode={false}
|
isSelectionMode={false}
|
||||||
|
|
|
||||||
|
|
@ -15,7 +15,6 @@
|
||||||
{
|
{
|
||||||
segment: PhotostreamSegment;
|
segment: PhotostreamSegment;
|
||||||
scrollToFunction: (top: number) => void;
|
scrollToFunction: (top: number) => void;
|
||||||
onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
>;
|
>;
|
||||||
|
|
@ -107,44 +106,13 @@
|
||||||
updateSlidingWindow();
|
updateSlidingWindow();
|
||||||
};
|
};
|
||||||
|
|
||||||
const scrollBy = (y: number) => {
|
|
||||||
if (element) {
|
|
||||||
element.scrollBy(0, y);
|
|
||||||
}
|
|
||||||
updateSlidingWindow();
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleTriggeredScrollCompensation = (compensation: { heightDelta?: number; scrollTop?: number }) => {
|
|
||||||
const { heightDelta, scrollTop } = compensation;
|
|
||||||
if (heightDelta !== undefined) {
|
|
||||||
scrollBy(heightDelta);
|
|
||||||
} else if (scrollTop !== undefined) {
|
|
||||||
scrollTo(scrollTop);
|
|
||||||
}
|
|
||||||
timelineManager.clearScrollCompensation();
|
|
||||||
};
|
|
||||||
|
|
||||||
const getAssetHeight = (assetId: string, monthGroup: PhotostreamSegment) => {
|
|
||||||
// the following method may trigger any layouts, so need to
|
|
||||||
// handle any scroll compensation that may have been set
|
|
||||||
const height = monthGroup.findAssetAbsolutePosition(assetId);
|
|
||||||
|
|
||||||
// this is in a while loop, since scrollCompensations invoke scrolls
|
|
||||||
// which may load months, triggering more scrollCompensations. Call
|
|
||||||
// this in a loop, until no more layouts occur.
|
|
||||||
while (timelineManager.scrollCompensation.monthGroup) {
|
|
||||||
handleTriggeredScrollCompensation(timelineManager.scrollCompensation);
|
|
||||||
}
|
|
||||||
return height;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const scrollToAssetId = async (assetId: string) => {
|
export const scrollToAssetId = async (assetId: string) => {
|
||||||
const monthGroup = await timelineManager.findSegmentForAssetId(assetId);
|
const monthGroup = await timelineManager.findSegmentForAssetId(assetId);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const height = getAssetHeight(assetId, monthGroup);
|
const height = monthGroup.findAssetAbsolutePosition(assetId);
|
||||||
scrollTo(height);
|
scrollTo(height);
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
@ -274,7 +242,6 @@
|
||||||
{@render segment({
|
{@render segment({
|
||||||
segment: monthGroup,
|
segment: monthGroup,
|
||||||
scrollToFunction: scrollTo,
|
scrollToFunction: scrollTo,
|
||||||
onScrollCompensationMonthInDOM: handleTriggeredScrollCompensation,
|
|
||||||
})}
|
})}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,6 @@
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
segment: PhotostreamSegment;
|
segment: PhotostreamSegment;
|
||||||
onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
>;
|
>;
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,6 @@
|
||||||
import { navigate } from '$lib/utils/navigation';
|
import { navigate } from '$lib/utils/navigation';
|
||||||
|
|
||||||
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
||||||
import type { PhotostreamSegment } from '$lib/managers/photostream-manager/PhotostreamSegment.svelte';
|
|
||||||
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||||
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
|
||||||
import { searchStore } from '$lib/stores/search.svelte';
|
import { searchStore } from '$lib/stores/search.svelte';
|
||||||
|
|
@ -21,28 +20,17 @@
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
>;
|
>;
|
||||||
segment: PhotostreamSegment;
|
|
||||||
isSelectionMode: boolean;
|
isSelectionMode: boolean;
|
||||||
singleSelect: boolean;
|
singleSelect: boolean;
|
||||||
timelineManager: PhotostreamManager;
|
timelineManager: PhotostreamManager;
|
||||||
assetInteraction: AssetInteraction;
|
assetInteraction: AssetInteraction;
|
||||||
onAssetOpen?: (asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
onAssetOpen?: (asset: TimelineAsset, defaultAssetOpen: () => void) => void;
|
||||||
onAssetSelect?: (asset: TimelineAsset) => void;
|
onAssetSelect?: (asset: TimelineAsset) => void;
|
||||||
|
|
||||||
onScrollCompensationMonthInDOM: (compensation: { heightDelta?: number; scrollTop?: number }) => void;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let {
|
let { content, isSelectionMode, singleSelect, assetInteraction, timelineManager, onAssetOpen, onAssetSelect }: Props =
|
||||||
segment,
|
$props();
|
||||||
content,
|
|
||||||
isSelectionMode,
|
|
||||||
singleSelect,
|
|
||||||
assetInteraction,
|
|
||||||
timelineManager,
|
|
||||||
onAssetOpen,
|
|
||||||
onAssetSelect,
|
|
||||||
onScrollCompensationMonthInDOM,
|
|
||||||
}: Props = $props();
|
|
||||||
|
|
||||||
let shiftKeyIsDown = $state(false);
|
let shiftKeyIsDown = $state(false);
|
||||||
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
let isEmpty = $derived(timelineManager.isInitialized && timelineManager.months.length === 0);
|
||||||
|
|
@ -189,12 +177,6 @@
|
||||||
const assets = assetsSnapshot(timelineManager.retrieveLoadedRange(startAsset, endAsset));
|
const assets = assetsSnapshot(timelineManager.retrieveLoadedRange(startAsset, endAsset));
|
||||||
assetInteraction.setAssetSelectionCandidates(assets);
|
assetInteraction.setAssetSelectionCandidates(assets);
|
||||||
};
|
};
|
||||||
|
|
||||||
$effect.root(() => {
|
|
||||||
if (timelineManager.scrollCompensation.monthGroup === segment) {
|
|
||||||
onScrollCompensationMonthInDOM(timelineManager.scrollCompensation);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
<svelte:document onkeydown={onKeyDown} onkeyup={onKeyUp} />
|
||||||
|
|
|
||||||
|
|
@ -99,10 +99,8 @@
|
||||||
title={(segment as MonthGroup).monthGroupTitle}
|
title={(segment as MonthGroup).monthGroupTitle}
|
||||||
/>
|
/>
|
||||||
{/snippet}
|
{/snippet}
|
||||||
{#snippet segment({ segment, onScrollCompensationMonthInDOM })}
|
{#snippet segment({ segment })}
|
||||||
<SelectableSegment
|
<SelectableSegment
|
||||||
{segment}
|
|
||||||
{onScrollCompensationMonthInDOM}
|
|
||||||
{timelineManager}
|
{timelineManager}
|
||||||
{assetInteraction}
|
{assetInteraction}
|
||||||
{isSelectionMode}
|
{isSelectionMode}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
|
import { updateIntersectionMonthGroup } 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 { CancellableTask } from '$lib/utils/cancellable-task';
|
import { CancellableTask } from '$lib/utils/cancellable-task';
|
||||||
import { clamp, debounce } from 'lodash-es';
|
import { debounce } from 'lodash-es';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
PhotostreamSegment,
|
PhotostreamSegment,
|
||||||
|
|
@ -21,15 +21,14 @@ import { setDifference } from '$lib/utils/timeline-util';
|
||||||
|
|
||||||
export abstract class PhotostreamManager {
|
export abstract class PhotostreamManager {
|
||||||
isInitialized = $state(false);
|
isInitialized = $state(false);
|
||||||
|
|
||||||
topSectionHeight = $state(0);
|
topSectionHeight = $state(0);
|
||||||
bottomSectionHeight = $state(60);
|
bottomSectionHeight = $state(60);
|
||||||
abstract get months(): PhotostreamSegment[];
|
|
||||||
|
assetCount = $derived.by(() => this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
|
||||||
timelineHeight = $derived.by(
|
timelineHeight = $derived.by(
|
||||||
() => this.months.reduce((accumulator, b) => accumulator + b.height, 0) + this.topSectionHeight,
|
() => this.months.reduce((accumulator, b) => accumulator + b.height, 0) + this.topSectionHeight,
|
||||||
);
|
);
|
||||||
assetCount = $derived.by(() => this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
|
|
||||||
|
|
||||||
topIntersectingMonthGroup: PhotostreamSegment | undefined = $state();
|
|
||||||
|
|
||||||
visibleWindow = $derived.by(() => ({
|
visibleWindow = $derived.by(() => ({
|
||||||
top: this.#scrollTop,
|
top: this.#scrollTop,
|
||||||
|
|
@ -54,17 +53,9 @@ export abstract class PhotostreamManager {
|
||||||
#suspendTransitions = $state(false);
|
#suspendTransitions = $state(false);
|
||||||
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
|
#resetScrolling = debounce(() => (this.#scrolling = false), 1000);
|
||||||
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
|
#resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
|
||||||
scrollCompensation: {
|
#updatingIntersections = false;
|
||||||
heightDelta: number | undefined;
|
|
||||||
scrollTop: number | undefined;
|
|
||||||
monthGroup: PhotostreamSegment | undefined;
|
|
||||||
} = $state({
|
|
||||||
heightDelta: 0,
|
|
||||||
scrollTop: 0,
|
|
||||||
monthGroup: undefined,
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor() {}
|
abstract get months(): PhotostreamSegment[];
|
||||||
|
|
||||||
setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) {
|
setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) {
|
||||||
let changed = false;
|
let changed = false;
|
||||||
|
|
@ -163,39 +154,17 @@ export abstract class PhotostreamManager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
clearScrollCompensation() {
|
|
||||||
this.scrollCompensation = {
|
|
||||||
heightDelta: undefined,
|
|
||||||
scrollTop: undefined,
|
|
||||||
monthGroup: undefined,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
updateIntersections() {
|
updateIntersections() {
|
||||||
if (!this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
|
if (this.#updatingIntersections || !this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let topIntersectingMonthGroup = undefined;
|
this.#updatingIntersections = true;
|
||||||
|
|
||||||
for (const month of this.months) {
|
for (const month of this.months) {
|
||||||
updateIntersectionMonthGroup(this, month);
|
updateIntersectionMonthGroup(this, month);
|
||||||
if (!topIntersectingMonthGroup && month.actuallyIntersecting) {
|
|
||||||
topIntersectingMonthGroup = month;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (topIntersectingMonthGroup !== undefined && this.topIntersectingMonthGroup !== topIntersectingMonthGroup) {
|
|
||||||
this.topIntersectingMonthGroup = topIntersectingMonthGroup;
|
|
||||||
}
|
|
||||||
for (const month of this.months) {
|
|
||||||
if (month === this.topIntersectingMonthGroup) {
|
|
||||||
this.topIntersectingMonthGroup.percent = clamp(
|
|
||||||
(this.visibleWindow.top - this.topIntersectingMonthGroup.top) / this.topIntersectingMonthGroup.height,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
month.percent = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.#updatingIntersections = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
async init() {
|
async init() {
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ import { t } from 'svelte-i18n';
|
||||||
import { get } from 'svelte/store';
|
import { get } from 'svelte/store';
|
||||||
|
|
||||||
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
import type { PhotostreamManager } from '$lib/managers/photostream-manager/PhotostreamManager.svelte';
|
||||||
|
import { getTestHook } from '$lib/managers/photostream-manager/TestHooks.svelte';
|
||||||
import type { AssetOperation, MoveAsset, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
import type { AssetOperation, MoveAsset, TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||||
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
import type { ViewerAsset } from '$lib/managers/timeline-manager/viewer-asset.svelte';
|
||||||
|
|
||||||
|
|
@ -20,7 +21,6 @@ export abstract class PhotostreamSegment {
|
||||||
#assets = $derived.by(() => this.viewerAssets.map((viewerAsset) => viewerAsset.asset));
|
#assets = $derived.by(() => this.viewerAssets.map((viewerAsset) => viewerAsset.asset));
|
||||||
|
|
||||||
initialCount = $state(0);
|
initialCount = $state(0);
|
||||||
percent = $state(0);
|
|
||||||
|
|
||||||
assetsCount = $derived.by(() => (this.isLoaded ? this.viewerAssets.length : this.initialCount));
|
assetsCount = $derived.by(() => (this.isLoaded ? this.viewerAssets.length : this.initialCount));
|
||||||
loader = new CancellableTask(
|
loader = new CancellableTask(
|
||||||
|
|
@ -30,6 +30,10 @@ export abstract class PhotostreamSegment {
|
||||||
);
|
);
|
||||||
isHeightActual = $state(false);
|
isHeightActual = $state(false);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
getTestHook()?.hookSegment(this);
|
||||||
|
}
|
||||||
|
|
||||||
abstract get timelineManager(): PhotostreamManager;
|
abstract get timelineManager(): PhotostreamManager;
|
||||||
|
|
||||||
abstract get identifier(): SegmentIdentifier;
|
abstract get identifier(): SegmentIdentifier;
|
||||||
|
|
@ -66,9 +70,13 @@ export abstract class PhotostreamSegment {
|
||||||
}
|
}
|
||||||
|
|
||||||
async load(cancelable: boolean): Promise<'DONE' | 'WAITED' | 'CANCELED' | 'LOADED' | 'ERRORED'> {
|
async load(cancelable: boolean): Promise<'DONE' | 'WAITED' | 'CANCELED' | 'LOADED' | 'ERRORED'> {
|
||||||
return await this.loader.execute(async (signal: AbortSignal) => {
|
const executionStatus = await this.loader.execute(async (signal: AbortSignal) => {
|
||||||
await this.fetch(signal);
|
await this.fetch(signal);
|
||||||
}, cancelable);
|
}, cancelable);
|
||||||
|
if (executionStatus === 'LOADED') {
|
||||||
|
this.layout();
|
||||||
|
}
|
||||||
|
return executionStatus;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected abstract fetch(signal: AbortSignal): Promise<void>;
|
protected abstract fetch(signal: AbortSignal): Promise<void>;
|
||||||
|
|
@ -88,42 +96,34 @@ export abstract class PhotostreamSegment {
|
||||||
if (this.#height === height) {
|
if (this.#height === height) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { timelineManager: store, percent } = this;
|
|
||||||
const index = store.months.indexOf(this);
|
let needsIntersectionUpdate = false;
|
||||||
|
const timelineManager = this.timelineManager;
|
||||||
|
const index = timelineManager.months.indexOf(this);
|
||||||
const heightDelta = height - this.#height;
|
const heightDelta = height - this.#height;
|
||||||
this.#height = height;
|
this.#height = height;
|
||||||
const prevMonthGroup = store.months[index - 1];
|
const prevMonthGroup = timelineManager.months[index - 1];
|
||||||
if (prevMonthGroup) {
|
if (prevMonthGroup) {
|
||||||
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
|
const newTop = prevMonthGroup.#top + prevMonthGroup.#height;
|
||||||
if (this.#top !== newTop) {
|
if (this.#top !== newTop) {
|
||||||
this.#top = newTop;
|
this.#top = newTop;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (let cursor = index + 1; cursor < store.months.length; cursor++) {
|
if (heightDelta === 0) {
|
||||||
const monthGroup = this.timelineManager.months[cursor];
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let cursor = index + 1; cursor < timelineManager.months.length; cursor++) {
|
||||||
|
const monthGroup = timelineManager.months[cursor];
|
||||||
const newTop = monthGroup.#top + heightDelta;
|
const newTop = monthGroup.#top + heightDelta;
|
||||||
if (monthGroup.#top !== newTop) {
|
if (monthGroup.#top !== newTop) {
|
||||||
monthGroup.#top = newTop;
|
monthGroup.#top = newTop;
|
||||||
|
needsIntersectionUpdate = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (store.topIntersectingMonthGroup) {
|
|
||||||
const currentIndex = store.months.indexOf(store.topIntersectingMonthGroup);
|
if (needsIntersectionUpdate) {
|
||||||
if (currentIndex > 0) {
|
timelineManager.updateIntersections();
|
||||||
if (index < currentIndex) {
|
|
||||||
store.scrollCompensation = {
|
|
||||||
heightDelta,
|
|
||||||
scrollTop: undefined,
|
|
||||||
monthGroup: this,
|
|
||||||
};
|
|
||||||
} else if (percent > 0) {
|
|
||||||
const top = this.top + height * percent;
|
|
||||||
store.scrollCompensation = {
|
|
||||||
heightDelta: undefined,
|
|
||||||
scrollTop: top,
|
|
||||||
monthGroup: this,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,11 @@
|
||||||
|
import type { PhotostreamSegment } from '$lib/managers/photostream-manager/PhotostreamSegment.svelte';
|
||||||
|
|
||||||
|
let testHooks: { hookSegment: (segment: PhotostreamSegment) => void } | undefined = undefined;
|
||||||
|
|
||||||
|
export function setTestHook(hooks: { hookSegment: (segment: PhotostreamSegment) => void }) {
|
||||||
|
testHooks = hooks;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getTestHook() {
|
||||||
|
return testHooks;
|
||||||
|
}
|
||||||
|
|
@ -51,7 +51,7 @@ export function calculateSegmentIntersecting(
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Calculate intersection for viewer assets with additional parameters like header height and scroll compensation
|
* Calculate intersection for viewer assets with additional parameters like header height
|
||||||
*/
|
*/
|
||||||
export function calculateViewerAssetIntersecting(
|
export function calculateViewerAssetIntersecting(
|
||||||
timelineManager: PhotostreamManager,
|
timelineManager: PhotostreamManager,
|
||||||
|
|
@ -60,13 +60,8 @@ export function calculateViewerAssetIntersecting(
|
||||||
expandTop: number = INTERSECTION_EXPAND_TOP,
|
expandTop: number = INTERSECTION_EXPAND_TOP,
|
||||||
expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
|
expandBottom: number = INTERSECTION_EXPAND_BOTTOM,
|
||||||
) {
|
) {
|
||||||
const scrollCompensationHeightDelta = timelineManager.scrollCompensation?.heightDelta ?? 0;
|
const topWindow = timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop;
|
||||||
|
const bottomWindow = timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom;
|
||||||
const topWindow =
|
|
||||||
timelineManager.visibleWindow.top - timelineManager.headerHeight - expandTop + scrollCompensationHeightDelta;
|
|
||||||
const bottomWindow =
|
|
||||||
timelineManager.visibleWindow.bottom + timelineManager.headerHeight + expandBottom + scrollCompensationHeightDelta;
|
|
||||||
|
|
||||||
const positionBottom = positionTop + positionHeight;
|
const positionBottom = positionTop + positionHeight;
|
||||||
|
|
||||||
return isIntersecting(positionTop, positionBottom, topWindow, bottomWindow);
|
return isIntersecting(positionTop, positionBottom, topWindow, bottomWindow);
|
||||||
|
|
|
||||||
|
|
@ -73,6 +73,10 @@ export class MonthGroup extends PhotostreamSegment {
|
||||||
return loadFromTimeBuckets(this.timelineManager, this, this.timelineManager.options, signal);
|
return loadFromTimeBuckets(this.timelineManager, this, this.timelineManager.options, signal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
layout(noDefer?: boolean) {
|
||||||
|
layoutMonthGroup(this.timelineManager, this, noDefer);
|
||||||
|
}
|
||||||
|
|
||||||
get lastDayGroup() {
|
get lastDayGroup() {
|
||||||
return this.dayGroups.at(-1);
|
return this.dayGroups.at(-1);
|
||||||
}
|
}
|
||||||
|
|
@ -306,10 +310,6 @@ export class MonthGroup extends PhotostreamSegment {
|
||||||
this.loader?.cancel();
|
this.loader?.cancel();
|
||||||
}
|
}
|
||||||
|
|
||||||
layout(noDefer?: boolean) {
|
|
||||||
layoutMonthGroup(this.timelineManager, this, noDefer);
|
|
||||||
}
|
|
||||||
|
|
||||||
#clearDeferredLayout() {
|
#clearDeferredLayout() {
|
||||||
const hasDeferred = this.dayGroups.some((group) => group.deferredLayout);
|
const hasDeferred = this.dayGroups.some((group) => group.deferredLayout);
|
||||||
if (hasDeferred) {
|
if (hasDeferred) {
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
import { sdkMock } from '$lib/__mocks__/sdk.mock';
|
||||||
|
import type { PhotostreamSegment } from '$lib/managers/photostream-manager/PhotostreamSegment.svelte';
|
||||||
|
import { setTestHook } from '$lib/managers/photostream-manager/TestHooks.svelte';
|
||||||
import {
|
import {
|
||||||
findMonthGroupForAsset,
|
findMonthGroupForAsset,
|
||||||
getMonthGroupByDate,
|
getMonthGroupByDate,
|
||||||
|
|
@ -7,6 +9,7 @@ import { AbortError } from '$lib/utils';
|
||||||
import { fromISODateTimeUTCToObject, getSegmentIdentifier } from '$lib/utils/timeline-util';
|
import { fromISODateTimeUTCToObject, getSegmentIdentifier } from '$lib/utils/timeline-util';
|
||||||
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
import { type AssetResponseDto, type TimeBucketAssetResponseDto } from '@immich/sdk';
|
||||||
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
import { timelineAssetFactory, toResponseDto } from '@test-data/factories/asset-factory';
|
||||||
|
import type { MockedFunction } from 'vitest';
|
||||||
import { TimelineManager } from './timeline-manager.svelte';
|
import { TimelineManager } from './timeline-manager.svelte';
|
||||||
import type { TimelineAsset } from './types';
|
import type { TimelineAsset } from './types';
|
||||||
|
|
||||||
|
|
@ -57,8 +60,16 @@ describe('TimelineManager', () => {
|
||||||
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
Object.entries(bucketAssets).map(([key, assets]) => [key, toResponseDto(...assets)]),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const spys: { segment: PhotostreamSegment; cancelSpy: MockedFunction<() => void> }[] = [];
|
||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
timelineManager = new TimelineManager();
|
timelineManager = new TimelineManager();
|
||||||
|
|
||||||
|
setTestHook({
|
||||||
|
hookSegment: (segment) => {
|
||||||
|
spys.push({ segment, cancelSpy: vi.spyOn(segment, 'cancel') as MockedFunction<() => void> });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
sdkMock.getTimeBuckets.mockResolvedValue([
|
sdkMock.getTimeBuckets.mockResolvedValue([
|
||||||
{ count: 1, timeBucket: '2024-03-01' },
|
{ count: 1, timeBucket: '2024-03-01' },
|
||||||
{ count: 100, timeBucket: '2024-02-01' },
|
{ count: 100, timeBucket: '2024-02-01' },
|
||||||
|
|
@ -71,7 +82,7 @@ describe('TimelineManager', () => {
|
||||||
|
|
||||||
it('should load months in viewport', () => {
|
it('should load months in viewport', () => {
|
||||||
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
expect(sdkMock.getTimeBuckets).toBeCalledTimes(1);
|
||||||
expect(sdkMock.getTimeBucket).toHaveBeenCalledTimes(2);
|
expect(spys[2].cancelSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates month height', () => {
|
it('calculates month height', () => {
|
||||||
|
|
@ -85,13 +96,13 @@ describe('TimelineManager', () => {
|
||||||
expect.arrayContaining([
|
expect.arrayContaining([
|
||||||
expect.objectContaining({ year: 2024, month: 3, height: 165.5 }),
|
expect.objectContaining({ year: 2024, month: 3, height: 165.5 }),
|
||||||
expect.objectContaining({ year: 2024, month: 2, height: 11_996 }),
|
expect.objectContaining({ year: 2024, month: 2, height: 11_996 }),
|
||||||
expect.objectContaining({ year: 2024, month: 1, height: 286 }),
|
expect.objectContaining({ year: 2024, month: 1, height: 48 }),
|
||||||
]),
|
]),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('calculates timeline height', () => {
|
it('calculates timeline height', () => {
|
||||||
expect(timelineManager.timelineHeight).toBe(12_447.5);
|
expect(timelineManager.timelineHeight).toBe(12_209.5);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue