mirror of
https://github.com/immich-app/immich.git
synced 2025-09-29 15:31:13 -04:00
refactor(web): Clarify property names in Timeline and Scrubber
Renamed properties across Timeline/Scrubber components for clarity: - scrubOverallPercent → timelineScrollPercent - scrubberMonthPercent → viewportTopMonthScrollPercent - scrubberMonth → viewportTopMonth - leadout → isInLeadOutSection Additional changes: - Updated ScrubberListener signature to accept object parameter - Added detailed JSDoc comments for all Scrubber props - Fixed callback invocations to use new object syntax - Aligned Timeline's local state variables with Scrubber prop names
This commit is contained in:
parent
bea116e1b9
commit
9051fa6949
@ -3,7 +3,7 @@
|
|||||||
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
import type { ScrubberMonth } from '$lib/managers/timeline-manager/types';
|
||||||
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
|
||||||
import { getTabbable } from '$lib/utils/focus-util';
|
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 { Icon } from '@immich/ui';
|
import { Icon } from '@immich/ui';
|
||||||
import { mdiPlay } from '@mdi/js';
|
import { mdiPlay } from '@mdi/js';
|
||||||
import { clamp } from 'lodash-es';
|
import { clamp } from 'lodash-es';
|
||||||
@ -11,18 +11,31 @@
|
|||||||
import { fade, fly } from 'svelte/transition';
|
import { fade, fly } from 'svelte/transition';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
|
/** Offset from the top of the timeline (e.g., for headers) */
|
||||||
timelineTopOffset?: number;
|
timelineTopOffset?: number;
|
||||||
|
/** Offset from the bottom of the timeline (e.g., for footers) */
|
||||||
timelineBottomOffset?: number;
|
timelineBottomOffset?: number;
|
||||||
|
/** Total height of the scrubber component */
|
||||||
height?: number;
|
height?: number;
|
||||||
|
/** Timeline manager instance that controls the timeline state */
|
||||||
timelineManager: TimelineManager;
|
timelineManager: TimelineManager;
|
||||||
scrubOverallPercent?: number;
|
/** Overall scroll percentage through the entire timeline (0-1), used when no specific month is targeted */
|
||||||
scrubberMonthPercent?: number;
|
timelineScrollPercent?: number;
|
||||||
scrubberMonth?: { year: number; month: number };
|
/** The percentage of scroll through the month that is currently intersecting the top boundary of the viewport */
|
||||||
leadout?: boolean;
|
viewportTopMonthScrollPercent?: number;
|
||||||
|
/** The year/month of the timeline month at the top of the viewport */
|
||||||
|
viewportTopMonth?: TimelineYearMonth;
|
||||||
|
/** Indicates whether the viewport is currently in the lead-out section (after all months) */
|
||||||
|
isInLeadOutSection?: boolean;
|
||||||
|
/** Width of the scrubber component in pixels (bindable for parent component margin adjustments) */
|
||||||
scrubberWidth?: number;
|
scrubberWidth?: number;
|
||||||
|
/** Callback fired when user interacts with the scrubber to navigate */
|
||||||
onScrub?: ScrubberListener;
|
onScrub?: ScrubberListener;
|
||||||
|
/** Callback fired when keyboard events occur on the scrubber */
|
||||||
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
onScrubKeyDown?: (event: KeyboardEvent, element: HTMLElement) => void;
|
||||||
|
/** Callback fired when scrubbing starts */
|
||||||
startScrub?: ScrubberListener;
|
startScrub?: ScrubberListener;
|
||||||
|
/** Callback fired when scrubbing stops */
|
||||||
stopScrub?: ScrubberListener;
|
stopScrub?: ScrubberListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -31,10 +44,10 @@
|
|||||||
timelineBottomOffset = 0,
|
timelineBottomOffset = 0,
|
||||||
height = 0,
|
height = 0,
|
||||||
timelineManager,
|
timelineManager,
|
||||||
scrubOverallPercent = 0,
|
timelineScrollPercent = 0,
|
||||||
scrubberMonthPercent = 0,
|
viewportTopMonthScrollPercent = 0,
|
||||||
scrubberMonth = undefined,
|
viewportTopMonth = undefined,
|
||||||
leadout = false,
|
isInLeadOutSection = false,
|
||||||
onScrub = undefined,
|
onScrub = undefined,
|
||||||
onScrubKeyDown = undefined,
|
onScrubKeyDown = undefined,
|
||||||
startScrub = undefined,
|
startScrub = undefined,
|
||||||
@ -100,7 +113,7 @@
|
|||||||
offset += scrubberMonthPercent * relativeBottomOffset;
|
offset += scrubberMonthPercent * relativeBottomOffset;
|
||||||
}
|
}
|
||||||
return offset;
|
return offset;
|
||||||
} else if (leadout) {
|
} else if (isInLeadOutSection) {
|
||||||
let offset = relativeTopOffset;
|
let offset = relativeTopOffset;
|
||||||
for (const segment of segments) {
|
for (const segment of segments) {
|
||||||
offset += segment.height;
|
offset += segment.height;
|
||||||
@ -111,7 +124,9 @@
|
|||||||
return scrubOverallPercent * (height - (PADDING_TOP + PADDING_BOTTOM));
|
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 timelineFullHeight = $derived(timelineManager.scrubberTimelineHeight + timelineTopOffset + timelineBottomOffset);
|
||||||
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
let relativeTopOffset = $derived(toScrollY(timelineTopOffset / timelineFullHeight));
|
||||||
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
let relativeBottomOffset = $derived(toScrollY(timelineBottomOffset / timelineFullHeight));
|
||||||
@ -295,12 +310,24 @@
|
|||||||
|
|
||||||
const scrollPercent = toTimelineY(hoverY);
|
const scrollPercent = toTimelineY(hoverY);
|
||||||
if (wasDragging === false && isDragging) {
|
if (wasDragging === false && isDragging) {
|
||||||
void startScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void startScrub?.({
|
||||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
|
void onScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (wasDragging && !isDragging) {
|
if (wasDragging && !isDragging) {
|
||||||
void stopScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void stopScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -308,7 +335,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
void onScrub?.(segmentDate!, scrollPercent, monthGroupPercentY);
|
void onScrub?.({
|
||||||
|
scrubberMonth: segmentDate!,
|
||||||
|
overallScrollPercent: scrollPercent,
|
||||||
|
scrubberMonthScrollPercent: monthGroupPercentY,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
/* eslint-disable tscompat/tscompat */
|
/* eslint-disable tscompat/tscompat */
|
||||||
const getTouch = (event: TouchEvent) => {
|
const getTouch = (event: TouchEvent) => {
|
||||||
@ -412,7 +443,11 @@
|
|||||||
}
|
}
|
||||||
if (next) {
|
if (next) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
void onScrub?.({
|
||||||
|
scrubberMonth: { year: next.year, month: next.month },
|
||||||
|
overallScrollPercent: -1,
|
||||||
|
scrubberMonthScrollPercent: 0,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -422,7 +457,11 @@
|
|||||||
const next = segments[idx + 1];
|
const next = segments[idx + 1];
|
||||||
if (next) {
|
if (next) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
void onScrub?.({ year: next.year, month: next.month }, -1, 0);
|
void onScrub?.({
|
||||||
|
scrubberMonth: { year: next.year, month: next.month },
|
||||||
|
overallScrollPercent: -1,
|
||||||
|
scrubberMonthScrollPercent: 0,
|
||||||
|
});
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -110,14 +110,19 @@
|
|||||||
let timelineElement: HTMLElement | undefined = $state();
|
let timelineElement: HTMLElement | undefined = $state();
|
||||||
let showSkeleton = $state(true);
|
let showSkeleton = $state(true);
|
||||||
let isShowSelectDate = $state(false);
|
let isShowSelectDate = $state(false);
|
||||||
let scrubberMonthPercent = $state(0);
|
// The percentage of scroll through the month that is currently intersecting the top boundary of the viewport.
|
||||||
let scrubberMonth: { year: number; month: number } | undefined = $state(undefined);
|
// Note: There may be multiple months visible within the viewport at any given time.
|
||||||
let scrubOverallPercent: number = $state(0);
|
let viewportTopMonthScrollPercent = $state(0);
|
||||||
|
// The timeline month intersecting the top position of the viewport
|
||||||
|
let viewportTopMonth: { year: number; month: number } | undefined = $state(undefined);
|
||||||
|
// Overall scroll percentage through the entire timeline (0-1)
|
||||||
|
let timelineScrollPercent: number = $state(0);
|
||||||
let scrubberWidth = $state(0);
|
let scrubberWidth = $state(0);
|
||||||
|
|
||||||
// 60 is the bottom spacer element at 60px
|
// 60 is the bottom spacer element at 60px
|
||||||
let bottomSectionHeight = 60;
|
let bottomSectionHeight = 60;
|
||||||
let leadout = $state(false);
|
// Indicates whether the viewport is currently in the lead-out section (after all months)
|
||||||
|
let isInLeadOutSection = $state(false);
|
||||||
|
|
||||||
const maxMd = $derived(mobileDevice.maxMd);
|
const maxMd = $derived(mobileDevice.maxMd);
|
||||||
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
|
||||||
@ -301,20 +306,19 @@
|
|||||||
scrollTop(scrollToTop);
|
scrollTop(scrollToTop);
|
||||||
};
|
};
|
||||||
|
|
||||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
// note: don't throttle, debounce, or otherwise make this function async - it causes flicker
|
||||||
const onScrub: ScrubberListener = (
|
// this function scrolls the timeline to the specified month group and offset, based on scrubber interaction
|
||||||
scrubMonth: { year: number; month: number },
|
const onScrub: ScrubberListener = (scrubberData) => {
|
||||||
overallScrollPercent: number,
|
const { scrubberMonth, overallScrollPercent, scrubberMonthScrollPercent } = scrubberData;
|
||||||
scrubberMonthScrollPercent: number,
|
|
||||||
) => {
|
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||||
if (!scrubMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
|
||||||
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
const offset = maxScroll * overallScrollPercent;
|
const offset = maxScroll * overallScrollPercent;
|
||||||
scrollTop(offset);
|
scrollTop(offset);
|
||||||
} else {
|
} else {
|
||||||
const monthGroup = timelineManager.months.find(
|
const monthGroup = timelineManager.months.find(
|
||||||
({ yearMonth: { year, month } }) => year === scrubMonth.year && month === scrubMonth.month,
|
({ yearMonth: { year, month } }) => year === scrubberMonth.year && month === scrubberMonth.month,
|
||||||
);
|
);
|
||||||
if (!monthGroup) {
|
if (!monthGroup) {
|
||||||
return;
|
return;
|
||||||
@ -325,7 +329,7 @@
|
|||||||
|
|
||||||
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
|
||||||
const handleTimelineScroll = () => {
|
const handleTimelineScroll = () => {
|
||||||
leadout = false;
|
isInLeadOutSection = false;
|
||||||
|
|
||||||
if (!element) {
|
if (!element) {
|
||||||
return;
|
return;
|
||||||
@ -334,19 +338,19 @@
|
|||||||
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2) {
|
||||||
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||||
|
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
} else {
|
} else {
|
||||||
let top = element.scrollTop;
|
let top = element.scrollTop;
|
||||||
if (top < timelineManager.topSectionHeight) {
|
if (top < timelineManager.topSectionHeight) {
|
||||||
// in the lead-in area
|
// in the lead-in area
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
const maxScroll = getMaxScroll();
|
const maxScroll = getMaxScroll();
|
||||||
|
|
||||||
scrubOverallPercent = Math.min(1, element.scrollTop / maxScroll);
|
timelineScrollPercent = Math.min(1, element.scrollTop / maxScroll);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -371,15 +375,15 @@
|
|||||||
let next = top - monthGroupHeight * maxScrollPercent;
|
let next = top - monthGroupHeight * maxScrollPercent;
|
||||||
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
// instead of checking for < 0, add a little wiggle room for subpixel resolution
|
||||||
if (next < -1 && monthGroup) {
|
if (next < -1 && monthGroup) {
|
||||||
scrubberMonth = monthGroup;
|
viewportTopMonth = monthGroup;
|
||||||
|
|
||||||
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
// allowing next to be at least 1 may cause percent to go negative, so ensure positive percentage
|
||||||
scrubberMonthPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
viewportTopMonthScrollPercent = Math.max(0, top / (monthGroupHeight * maxScrollPercent));
|
||||||
|
|
||||||
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
// compensate for lost precision/rounding errors advance to the next bucket, if present
|
||||||
if (scrubberMonthPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
if (viewportTopMonthScrollPercent > 0.9999 && i + 1 < monthsLength - 1) {
|
||||||
scrubberMonth = timelineManager.months[i + 1].yearMonth;
|
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
found = true;
|
found = true;
|
||||||
@ -388,10 +392,10 @@
|
|||||||
top = next;
|
top = next;
|
||||||
}
|
}
|
||||||
if (!found) {
|
if (!found) {
|
||||||
leadout = true;
|
isInLeadOutSection = true;
|
||||||
scrubberMonth = undefined;
|
viewportTopMonth = undefined;
|
||||||
scrubberMonthPercent = 0;
|
viewportTopMonthScrollPercent = 0;
|
||||||
scrubOverallPercent = 1;
|
timelineScrollPercent = 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -854,10 +858,10 @@
|
|||||||
height={timelineManager.viewportHeight}
|
height={timelineManager.viewportHeight}
|
||||||
timelineTopOffset={timelineManager.topSectionHeight}
|
timelineTopOffset={timelineManager.topSectionHeight}
|
||||||
timelineBottomOffset={bottomSectionHeight}
|
timelineBottomOffset={bottomSectionHeight}
|
||||||
{leadout}
|
{isInLeadOutSection}
|
||||||
{scrubOverallPercent}
|
{timelineScrollPercent}
|
||||||
{scrubberMonthPercent}
|
{viewportTopMonthScrollPercent}
|
||||||
{scrubberMonth}
|
{viewportTopMonth}
|
||||||
{onScrub}
|
{onScrub}
|
||||||
bind:scrubberWidth
|
bind:scrubberWidth
|
||||||
onScrubKeyDown={(evt) => {
|
onScrubKeyDown={(evt) => {
|
||||||
|
@ -23,11 +23,11 @@ export type TimelineDateTime = TimelineDate & {
|
|||||||
millisecond: number;
|
millisecond: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type ScrubberListener = (
|
export type ScrubberListener = (scrubberData: {
|
||||||
scrubberMonth: { year: number; month: number },
|
scrubberMonth: { year: number; month: number };
|
||||||
overallScrollPercent: number,
|
overallScrollPercent: number;
|
||||||
scrubberMonthScrollPercent: number,
|
scrubberMonthScrollPercent: number;
|
||||||
) => void | Promise<void>;
|
}) => void | Promise<void>;
|
||||||
|
|
||||||
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
// used for AssetResponseDto.dateTimeOriginal, amongst others
|
||||||
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
export const fromISODateTime = (isoDateTime: string, timeZone: string): DateTime<true> =>
|
||||||
|
Loading…
x
Reference in New Issue
Block a user