mirror of
https://github.com/immich-app/immich.git
synced 2026-02-04 17:01:13 +03:00
use binary search for perf, refactor, improve readability
This commit is contained in:
@@ -5,7 +5,7 @@
|
||||
import type { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
|
||||
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
|
||||
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
|
||||
import type { ScrubberListener, TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { findMonthAtScrollPosition, type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Scrubber from './scrubber.svelte';
|
||||
|
||||
@@ -51,11 +51,7 @@
|
||||
empty,
|
||||
}: Props = $props();
|
||||
|
||||
// Constants for timeline calculations
|
||||
const VIEWPORT_MULTIPLIER = 2; // Used to determine if timeline is "small"
|
||||
const SUBPIXEL_TOLERANCE = -1; // Tolerance for scroll position checks
|
||||
const NEAR_END_THRESHOLD = 0.9999; // Threshold for detecting near-end of month
|
||||
|
||||
|
||||
let isInLeadOutSection = $state(false);
|
||||
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||
@@ -91,11 +87,6 @@
|
||||
return timelineManager.timelineHeight < timelineManager.viewportHeight * VIEWPORT_MULTIPLIER;
|
||||
};
|
||||
|
||||
const isNearMonthBoundary = (progress: number) => {
|
||||
return progress > NEAR_END_THRESHOLD;
|
||||
};
|
||||
|
||||
|
||||
const resetScrubberMonth = () => {
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
@@ -116,76 +107,24 @@
|
||||
};
|
||||
|
||||
const handleMonthScroll = () => {
|
||||
const scrollTop = timelineManager.visibleWindow.top;
|
||||
const scrollPosition = timelineManager.visibleWindow.top;
|
||||
const months = timelineManager.months;
|
||||
const maxScrollPercent = timelineManager.getMaxScrollPercent();
|
||||
|
||||
// Early exit if no months
|
||||
if (months.length === 0) {
|
||||
isInLeadOutSection = true;
|
||||
timelineScrollPercent = 1;
|
||||
resetScrubberMonth();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're before the first month (in lead-in)
|
||||
const firstMonthTop = months[0].top * maxScrollPercent;
|
||||
if (scrollTop < firstMonthTop - SUBPIXEL_TOLERANCE) {
|
||||
isInLeadOutSection = true;
|
||||
timelineScrollPercent = 1;
|
||||
resetScrubberMonth();
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we're after the last month (in lead-out)
|
||||
const lastMonth = months[months.length - 1];
|
||||
const lastMonthBottom = (lastMonth.top + lastMonth.height) * maxScrollPercent;
|
||||
if (scrollTop >= lastMonthBottom - SUBPIXEL_TOLERANCE) {
|
||||
isInLeadOutSection = true;
|
||||
timelineScrollPercent = 1;
|
||||
resetScrubberMonth();
|
||||
return;
|
||||
}
|
||||
|
||||
// Binary search to find the month containing the viewport top
|
||||
let left = 0;
|
||||
let right = months.length - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const month = months[mid];
|
||||
const monthTop = month.top * maxScrollPercent;
|
||||
const monthBottom = monthTop + month.height * maxScrollPercent;
|
||||
|
||||
if (scrollTop >= monthTop - SUBPIXEL_TOLERANCE && scrollTop < monthBottom - SUBPIXEL_TOLERANCE) {
|
||||
// Found the month containing the viewport top
|
||||
viewportTopMonth = month.yearMonth;
|
||||
const distanceIntoMonth = scrollTop - monthTop;
|
||||
viewportTopMonthScrollPercent = Math.max(0, distanceIntoMonth / (month.height * maxScrollPercent));
|
||||
|
||||
// Handle month boundary edge case
|
||||
if (isNearMonthBoundary(viewportTopMonthScrollPercent) && mid < months.length - 1) {
|
||||
viewportTopMonth = months[mid + 1].yearMonth;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
}
|
||||
|
||||
isInLeadOutSection = false;
|
||||
return;
|
||||
}
|
||||
|
||||
if (scrollTop < monthTop) {
|
||||
right = mid - 1;
|
||||
} else {
|
||||
left = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't reach here, but if we do, we're in lead-out
|
||||
isInLeadOutSection = true;
|
||||
timelineScrollPercent = 1;
|
||||
resetScrubberMonth();
|
||||
};
|
||||
|
||||
// Find the month at the current scroll position
|
||||
const searchResult = findMonthAtScrollPosition(months, scrollPosition, maxScrollPercent);
|
||||
|
||||
if (searchResult) {
|
||||
viewportTopMonth = searchResult.month;
|
||||
viewportTopMonthScrollPercent = searchResult.monthScrollPercent;
|
||||
isInLeadOutSection = false;
|
||||
} else {
|
||||
// We're in lead-out section
|
||||
isInLeadOutSection = true;
|
||||
timelineScrollPercent = 1;
|
||||
resetScrubberMonth();
|
||||
}
|
||||
};
|
||||
|
||||
const handleOverallPercentScroll = (percent: number, scrollTo?: (offset: number) => void) => {
|
||||
const maxScroll = timelineManager.getMaxScroll();
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<span class="w-full truncate first-letter:capitalize" title={dayGroup.groupTitle}>
|
||||
<span class="w-full truncate first-letter:capitalize" title={dayGroup.groupTitleFull}>
|
||||
{dayGroup.groupTitle}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -243,3 +243,79 @@ export function setDifference<T>(setA: Set<T>, setB: Set<T>): SvelteSet<T> {
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
export interface MonthGroupForSearch {
|
||||
yearMonth: TimelineYearMonth;
|
||||
top: number;
|
||||
height: number;
|
||||
}
|
||||
|
||||
export interface BinarySearchResult {
|
||||
month: TimelineYearMonth;
|
||||
monthScrollPercent: number;
|
||||
}
|
||||
|
||||
export function findMonthAtScrollPosition(
|
||||
months: MonthGroupForSearch[],
|
||||
scrollPosition: number,
|
||||
maxScrollPercent: number,
|
||||
): BinarySearchResult | null {
|
||||
const SUBPIXEL_TOLERANCE = -1; // Tolerance for scroll position checks
|
||||
const NEAR_END_THRESHOLD = 0.9999; // Threshold for detecting near-end of month
|
||||
|
||||
if (months.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we're before the first month
|
||||
const firstMonthTop = months[0].top * maxScrollPercent;
|
||||
if (scrollPosition < firstMonthTop - SUBPIXEL_TOLERANCE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Check if we're after the last month
|
||||
const lastMonth = months[months.length - 1];
|
||||
const lastMonthBottom = (lastMonth.top + lastMonth.height) * maxScrollPercent;
|
||||
if (scrollPosition >= lastMonthBottom - SUBPIXEL_TOLERANCE) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Binary search to find the month containing the scroll position
|
||||
let left = 0;
|
||||
let right = months.length - 1;
|
||||
|
||||
while (left <= right) {
|
||||
const mid = Math.floor((left + right) / 2);
|
||||
const month = months[mid];
|
||||
const monthTop = month.top * maxScrollPercent;
|
||||
const monthBottom = monthTop + month.height * maxScrollPercent;
|
||||
|
||||
if (scrollPosition >= monthTop - SUBPIXEL_TOLERANCE && scrollPosition < monthBottom - SUBPIXEL_TOLERANCE) {
|
||||
// Found the month containing the scroll position
|
||||
const distanceIntoMonth = scrollPosition - monthTop;
|
||||
let monthScrollPercent = Math.max(0, distanceIntoMonth / (month.height * maxScrollPercent));
|
||||
|
||||
// Handle month boundary edge case
|
||||
if (monthScrollPercent > NEAR_END_THRESHOLD && mid < months.length - 1) {
|
||||
return {
|
||||
month: months[mid + 1].yearMonth,
|
||||
monthScrollPercent: 0,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
month: month.yearMonth,
|
||||
monthScrollPercent,
|
||||
};
|
||||
}
|
||||
|
||||
if (scrollPosition < monthTop) {
|
||||
right = mid - 1;
|
||||
} else {
|
||||
left = mid + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Shouldn't reach here, but return null if we do
|
||||
return null;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user