mirror of
https://github.com/immich-app/immich.git
synced 2026-02-11 11:27:56 +03:00
refactor - improve timeline readability, general refactoring, renaming variables, functions, props, etc
This commit is contained in:
@@ -292,7 +292,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
{/each}
|
||||
<!-- spacer for leadout -->
|
||||
<!-- spacer for lead-out -->
|
||||
<div
|
||||
class="h-[60px]"
|
||||
style:position="absolute"
|
||||
|
||||
@@ -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 } from '$lib/utils/timeline-util';
|
||||
import type { ScrubberListener, TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import type { Snippet } from 'svelte';
|
||||
import Scrubber from './scrubber.svelte';
|
||||
|
||||
@@ -51,16 +51,34 @@
|
||||
empty,
|
||||
}: Props = $props();
|
||||
|
||||
let leadOut = $state(false);
|
||||
let scrubberMonthPercent = $state(0);
|
||||
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||
let scrubOverallPercent: number = $state(0);
|
||||
// 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
|
||||
|
||||
// Type for month section data
|
||||
interface MonthSection {
|
||||
height: number;
|
||||
monthGroup?: { year: number; month: number };
|
||||
type: 'lead-in' | 'lead-out' | 'month';
|
||||
}
|
||||
|
||||
// Constants for month loop bounds
|
||||
const MONTH_LOOP_START = -1; // Represents lead-in section
|
||||
const getMonthLoopEnd = (monthsLength: number) => monthsLength + 1; // +1 for lead-out
|
||||
|
||||
let isInLeadOutSection = $state(false);
|
||||
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||
// Note: There may be multiple months visible within the viewport at any given time.
|
||||
let viewportTopMonthScrollPercent = $state(0);
|
||||
let viewportTopMonth: TimelineYearMonth | undefined = $state(undefined);
|
||||
let timelineScrollPercent: number = $state(0);
|
||||
let scrubberWidth: number = $state(0);
|
||||
|
||||
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||
// this function updates the scrubber position based on the current scroll position in the timeline
|
||||
const handleTimelineScroll = () => {
|
||||
leadOut = false;
|
||||
isInLeadOutSection = false;
|
||||
|
||||
// Handle small timeline edge case: scroll limited due to size of content
|
||||
if (isSmallTimeline()) {
|
||||
@@ -80,111 +98,131 @@
|
||||
};
|
||||
|
||||
const isSmallTimeline = () => {
|
||||
return timelineManager.timelineHeight < timelineManager.viewportHeight * 2;
|
||||
return timelineManager.timelineHeight < timelineManager.viewportHeight * VIEWPORT_MULTIPLIER;
|
||||
};
|
||||
|
||||
const isNearMonthBoundary = (progress: number) => {
|
||||
return progress > NEAR_END_THRESHOLD;
|
||||
};
|
||||
|
||||
const canAdvanceToNextMonth = (currentIndex: number, monthsLength: number) => {
|
||||
return currentIndex + 1 < monthsLength - 1;
|
||||
};
|
||||
|
||||
const resetScrubberMonth = () => {
|
||||
scrubberMonth = undefined;
|
||||
scrubberMonthPercent = 0;
|
||||
viewportTopMonth = undefined;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
};
|
||||
|
||||
const calculateTimelineScrollPercent = () => {
|
||||
const maxScroll = timelineManager.getMaxScroll();
|
||||
timelineScrollPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
|
||||
resetScrubberMonth();
|
||||
};
|
||||
|
||||
const handleSmallTimelineScroll = () => {
|
||||
const maxScroll = timelineManager.getMaxScroll();
|
||||
scrubOverallPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
|
||||
resetScrubberMonth();
|
||||
calculateTimelineScrollPercent();
|
||||
};
|
||||
|
||||
const handleLeadInScroll = () => {
|
||||
const maxScroll = timelineManager.getMaxScroll();
|
||||
scrubOverallPercent = Math.min(1, timelineManager.visibleWindow.top / maxScroll);
|
||||
resetScrubberMonth();
|
||||
calculateTimelineScrollPercent();
|
||||
};
|
||||
|
||||
const handleMonthScroll = () => {
|
||||
const monthsLength = timelineManager.months.length;
|
||||
const maxScrollPercent = timelineManager.getMaxScrollPercent();
|
||||
let top = timelineManager.visibleWindow.top;
|
||||
let found = false;
|
||||
let remainingScrollDistance = timelineManager.visibleWindow.top;
|
||||
// Tracks if we found the month intersecting the viewport top
|
||||
let foundIntersectingMonth = false;
|
||||
|
||||
for (let i = -1; i < monthsLength + 1; i++) {
|
||||
const monthData = getMonthData(i);
|
||||
const next = top - monthData.height * maxScrollPercent;
|
||||
for (let i = MONTH_LOOP_START; i < getMonthLoopEnd(monthsLength); i++) {
|
||||
const monthSection = getMonthSection(i);
|
||||
const nextRemainingDistance = remainingScrollDistance - monthSection.height * maxScrollPercent;
|
||||
|
||||
// Check if we're in this month (with subpixel tolerance)
|
||||
if (next < -1 && monthData.monthGroup) {
|
||||
scrubberMonth = monthData.monthGroup;
|
||||
if (nextRemainingDistance < SUBPIXEL_TOLERANCE && monthSection.monthGroup) {
|
||||
viewportTopMonth = monthSection.monthGroup;
|
||||
|
||||
// Calculate month percentage
|
||||
scrubberMonthPercent = Math.max(0, top / (monthData.height * maxScrollPercent));
|
||||
// Calculate how far we've scrolled into this month as a percentage
|
||||
viewportTopMonthScrollPercent = Math.max(0, remainingScrollDistance / (monthSection.height * maxScrollPercent));
|
||||
|
||||
// Handle rounding errors (and/or subpixel tolerance) -
|
||||
// advance to next month if almost at end
|
||||
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||
scrubberMonth = timelineManager.months[i + 1].yearMonth;
|
||||
scrubberMonthPercent = 0;
|
||||
if (isNearMonthBoundary(viewportTopMonthScrollPercent) && canAdvanceToNextMonth(i, monthsLength)) {
|
||||
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
|
||||
viewportTopMonthScrollPercent = 0;
|
||||
}
|
||||
|
||||
found = true;
|
||||
foundIntersectingMonth = true;
|
||||
break;
|
||||
}
|
||||
top = next;
|
||||
remainingScrollDistance = nextRemainingDistance;
|
||||
}
|
||||
|
||||
if (!found) {
|
||||
leadOut = true;
|
||||
scrubOverallPercent = 1;
|
||||
if (!foundIntersectingMonth) {
|
||||
isInLeadOutSection = true;
|
||||
timelineScrollPercent = 1;
|
||||
resetScrubberMonth();
|
||||
}
|
||||
};
|
||||
|
||||
const getMonthData = (index: number) => {
|
||||
const getMonthSection = (index: number): MonthSection => {
|
||||
const monthsLength = timelineManager.months.length;
|
||||
|
||||
if (index === -1) {
|
||||
// lead-in
|
||||
if (index === MONTH_LOOP_START) {
|
||||
return {
|
||||
type: 'lead-in',
|
||||
height: timelineManager.topSectionHeight,
|
||||
monthGroup: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
if (index === monthsLength) {
|
||||
// lead-out
|
||||
return {
|
||||
type: 'lead-out',
|
||||
height: timelineManager.bottomSectionHeight,
|
||||
monthGroup: undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// normal month
|
||||
return {
|
||||
type: 'month',
|
||||
height: timelineManager.months[index].height,
|
||||
monthGroup: timelineManager.months[index].yearMonth,
|
||||
};
|
||||
};
|
||||
|
||||
const handleOverallPercentScroll = (percent: number, scrollTo?: (offset: number) => void) => {
|
||||
const maxScroll = timelineManager.getMaxScroll();
|
||||
const offset = maxScroll * percent;
|
||||
scrollTo?.(offset);
|
||||
};
|
||||
|
||||
const findMonthGroup = (target: TimelineYearMonth) => {
|
||||
return timelineManager.months.find(
|
||||
({ yearMonth }) => yearMonth.year === target.year && yearMonth.month === target.month,
|
||||
);
|
||||
};
|
||||
|
||||
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
|
||||
const onScrub: ScrubberListener = (scrubberData) => {
|
||||
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent, scrollToFunction } = scrubberData;
|
||||
|
||||
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||
const maxScroll = timelineManager.getMaxScroll();
|
||||
const offset = maxScroll * overallScrollPercent;
|
||||
scrollToFunction?.(offset);
|
||||
// Handle edge case or no month selected
|
||||
if (!scrubberMonth || isSmallTimeline()) {
|
||||
handleOverallPercentScroll(overallScrollPercent, scrollToFunction);
|
||||
return;
|
||||
}
|
||||
const monthGroup = timelineManager.months.find(
|
||||
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||
);
|
||||
if (!monthGroup) {
|
||||
return;
|
||||
|
||||
// Find and scroll to the selected month
|
||||
const monthGroup = findMonthGroup(scrubberMonth);
|
||||
if (monthGroup) {
|
||||
scrollToPositionWithinMonth(monthGroup, scrubberMonthScrollPercent, scrollToFunction);
|
||||
}
|
||||
scrollToMonthGroupAndOffset(monthGroup, scrubberMonthScrollPercent, scrollToFunction);
|
||||
};
|
||||
|
||||
const scrollToMonthGroupAndOffset = (
|
||||
const scrollToPositionWithinMonth = (
|
||||
monthGroup: MonthGroup,
|
||||
monthGroupScrollPercent: number,
|
||||
handleScrollTop?: (top: number) => void,
|
||||
@@ -218,18 +256,18 @@
|
||||
{empty}
|
||||
{handleTimelineScroll}
|
||||
>
|
||||
{#snippet header(scrollTo)}
|
||||
{#snippet header(scrollToFunction)}
|
||||
{#if timelineManager.months.length > 0}
|
||||
<Scrubber
|
||||
{timelineManager}
|
||||
height={timelineManager.viewportHeight}
|
||||
timelineTopOffset={timelineManager.topSectionHeight}
|
||||
timelineBottomOffset={timelineManager.bottomSectionHeight}
|
||||
{leadOut}
|
||||
{scrubOverallPercent}
|
||||
{scrubberMonthPercent}
|
||||
{scrubberMonth}
|
||||
onScrub={(args) => onScrub({ ...args, scrollToFunction: scrollTo })}
|
||||
{isInLeadOutSection}
|
||||
{timelineScrollPercent}
|
||||
{viewportTopMonthScrollPercent}
|
||||
{viewportTopMonth}
|
||||
onScrub={(scrubberData) => onScrub({ ...scrubberData, scrollToFunction })}
|
||||
bind:scrubberWidth
|
||||
/>
|
||||
{/if}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||
import { getTabbable } from '$lib/utils/focus-util';
|
||||
import { type ScrubberListener } from '$lib/utils/timeline-util';
|
||||
import { type ScrubberListener, type TimelineYearMonth } from '$lib/utils/timeline-util';
|
||||
import { mdiPlay } from '@mdi/js';
|
||||
import { clamp } from 'lodash-es';
|
||||
import { onMount } from 'svelte';
|
||||
@@ -15,10 +15,10 @@
|
||||
timelineBottomOffset?: number;
|
||||
height?: number;
|
||||
timelineManager: TimelineManager;
|
||||
scrubOverallPercent?: number;
|
||||
scrubberMonthPercent?: number;
|
||||
scrubberMonth?: { year: number; month: number };
|
||||
leadOut?: boolean;
|
||||
timelineScrollPercent?: number;
|
||||
viewportTopMonthScrollPercent?: number;
|
||||
viewportTopMonth?: TimelineYearMonth;
|
||||
isInLeadOutSection?: boolean;
|
||||
scrubberWidth?: number;
|
||||
onScrub?: ScrubberListener;
|
||||
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
||||
@@ -31,10 +31,10 @@
|
||||
timelineBottomOffset = 0,
|
||||
height = 0,
|
||||
timelineManager,
|
||||
scrubOverallPercent = 0,
|
||||
scrubberMonthPercent = 0,
|
||||
scrubberMonth = undefined,
|
||||
leadOut = false,
|
||||
timelineScrollPercent = 0,
|
||||
viewportTopMonthScrollPercent = 0,
|
||||
viewportTopMonth = undefined,
|
||||
isInLeadOutSection = false,
|
||||
onScrub = undefined,
|
||||
onScrubKeyDown = undefined,
|
||||
startScrub = undefined,
|
||||
@@ -100,7 +100,7 @@
|
||||
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||
}
|
||||
return offset;
|
||||
} else if (leadOut) {
|
||||
} else if (isInLeadOutSection) {
|
||||
let offset = relativeTopOffset;
|
||||
for (const segment of segments) {
|
||||
offset += segment.height;
|
||||
@@ -111,7 +111,9 @@
|
||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
||||
}
|
||||
};
|
||||
let scrollY = $derived(toScrollFromMonthGroupPercentage(scrubberMonth, scrubberMonthPercent, scrubOverallPercent));
|
||||
let scrollY = $derived(
|
||||
toScrollFromMonthGroupPercentage(viewportTopMonth, viewportTopMonthScrollPercent, timelineScrollPercent),
|
||||
);
|
||||
let timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||
|
||||
Reference in New Issue
Block a user