@ -12,14 +12,14 @@
import type { DayGroup } from '$lib/managers/timeline-manager/day-group.svelte';
import type { MonthGroup } from '$lib/managers/timeline-manager/month-group.svelte';
import { TimelineManager } from '$lib/managers/timeline-manager/timeline-manager.svelte';
import type { TimelineAsset } from '$lib/managers/timeline-manager/types';
import type { TimelineAsset , ViewportTopMonth } from '$lib/managers/timeline-manager/types';
import { assetsSnapshot } from '$lib/managers/timeline-manager/utils.svelte';
import type { AssetInteraction } from '$lib/stores/asset-interaction.svelte';
import { assetViewingStore } from '$lib/stores/asset-viewing.store';
import { isSelectingAllAssets } from '$lib/stores/assets-store.svelte';
import { mobileDevice } from '$lib/stores/mobile-device.svelte';
import { navigate } from '$lib/utils/navigation';
import { getTimes , type ScrubberListener , type TimelineYearMonth } from '$lib/utils/timeline-util';
import { getTimes , type ScrubberListener } from '$lib/utils/timeline-util';
import { type AlbumResponseDto , type PersonResponseDto } from '@immich/sdk';
import { DateTime } from 'luxon';
import { onMount , type Snippet } from 'svelte';
@ -97,16 +97,11 @@
// Note: There may be multiple months visible within the viewport at any given time.
let viewportTopMonthScrollPercent = $state(0);
// The timeline month intersecting the top position of the viewport
let viewportTopMonth: { year : number ; month : number } | undefined = $state(undefined);
let viewportTopMonth: ViewportTopMonth = $state(undefined);
// Overall scroll percentage through the entire timeline (0-1)
let timelineScrollPercent: number = $state(0);
let scrubberWidth = $state(0);
// 60 is the bottom spacer element at 60px
let bottomSectionHeight = 60;
// Indicates whether the viewport is currently in the lead-out section (after all months)
let isInLeadOutSection = $state(false);
const isEmpty = $derived(timelineManager.isInitialized & & timelineManager.months.length === 0);
const maxMd = $derived(mobileDevice.maxMd);
const usingMobileDevice = $derived(mobileDevice.pointerCoarse);
@ -230,41 +225,36 @@
}
});
const getMaxScrollPercent = () => {
const totalHeight = timelineManager.timelineHeight + bottomSectionHeight + timelineManager.topSectionHeight;
return (totalHeight - timelineManager.viewportHeight) / totalHeight;
};
const getMaxScroll = () => {
if (!scrollableElement || !timelineElement) {
return 0;
}
return (
timelineManager.topSectionHeight +
bottomSectionHeight +
(timelineElement.clientHeight - scrollableElement.clientHeight)
);
};
const scrollToMonthGroupAndOffset = (monthGroup: MonthGroup, monthGroupScrollPercent: number) => {
const topOffset = monthGroup.top;
const maxScrollPercent = getMaxScrollPercent();
const delta = monthGroup.height * monthGroupScrollPercent;
const scrollToSegmentPercentage = (segmentTop: number, segmentHeight: number, monthGroupScrollPercent: number) => {
const topOffset = segmentTop;
const maxScrollPercent = timelineManager.maxScrollPercent;
const delta = segmentHeight * monthGroupScrollPercent;
const scrollToTop = (topOffset + delta) * maxScrollPercent;
timelineManager.scrollTo(scrollToTop);
};
// 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 } = scrubberData;
if (!scrubberMonth || timelineManager.timelineHeight < timelineManager.viewportHeight * 2 ) {
const leadIn = scrubberMonth === 'lead-in';
const leadOut = scrubberMonth === 'lead-out';
const noMonth = !scrubberMonth;
if (noMonth || timelineManager.limitedScroll) {
// edge case - scroll limited due to size of content, must adjust - use use the overall percent instead
const maxScroll = getMaxScroll();
const offset = maxScroll * overallScrollPercent;
const maxScroll = timelineManager.maxScrollPercent;
const offset = maxScroll * overallScrollPercent * timelineManager.totalViewerHeight ;
timelineManager.scrollTo(offset);
} else if (leadIn) {
scrollToSegmentPercentage(0, timelineManager.topSectionHeight, scrubberMonthScrollPercent);
} else if (leadOut) {
scrollToSegmentPercentage(
timelineManager.topSectionHeight + timelineManager.assetsHeight,
timelineManager.bottomSectionHeight,
scrubberMonthScrollPercent,
);
} else {
const monthGroup = timelineManager.months.find(
({ yearMonth : { year , month } } ) => year === scrubberMonth.year && month === scrubberMonth.month,
@ -272,50 +262,41 @@
if (!monthGroup) {
return;
}
scrollToMonthGroupAndOffset(monthGroup , scrubberMonthScrollPercent);
scrollToSegmentPercentage(monthGroup.top, monthGroup.height , scrubberMonthScrollPercent);
}
};
// note: don't throttle, debounch, or otherwise make this function async - it causes flicker
const handleTimelineScroll = () => {
isInLeadOutSection = false;
if (!scrollableElement) {
return;
}
if (timelineManager.timelineHeight < timelineManager.viewportHeight * 2 ) {
if (timelineManager.limitedScroll) {
// edge case - scroll limited due to size of content, must adjust - use the overall percent instead
const maxScroll = getMaxScroll();
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
const maxScroll = timelineManager.maxScroll;
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
viewportTopMonth = undefined;
viewportTopMonthScrollPercent = 0;
} else {
let top = scrollableElement.scrollTop;
if (top < timelineManager.topSectionHeight ) {
// in the lead-in area
viewportTopMonth = undefined;
viewportTopMonthScrollPercent = 0;
const maxScroll = getMaxScroll();
timelineScrollPercent = Math.min(1, scrollableElement.scrollTop / maxScroll);
return;
}
timelineScrollPercent = 0;
let maxScrollPercent = getMaxScrollPercent() ;
let found = false ;
let top = scrollableElement.scrollTop;
let maxScrollPercent = timelineManager.maxScrollPercent;
const monthsLength = timelineManager.months.length;
for (let i = -1; i < monthsLength + 1 ; i ++) {
let monthGroup: TimelineYearMonth | undefined ;
let monthGroup: ViewportTopMonth;
let monthGroupHeight = 0;
if (i === -1) {
// lead-in
monthGroup = 'lead-in';
monthGroupHeight = timelineManager.topSectionHeight;
} else if (i === monthsLength) {
// lead-out
monthGroupHeight = bottomSectionHeight;
monthGroup = 'lead-out';
monthGroupHeight = timelineManager.bottomSectionHeight;
} else {
monthGroup = timelineManager.months[i].yearMonth;
monthGroupHeight = timelineManager.months[i].height;
@ -334,18 +315,10 @@
viewportTopMonth = timelineManager.months[i + 1].yearMonth;
viewportTopMonthScrollPercent = 0;
}
found = true;
break;
}
top = next;
}
if (!found) {
isInLeadOutSection = true;
viewportTopMonth = undefined;
viewportTopMonthScrollPercent = 0;
timelineScrollPercent = 1;
}
}
};
@ -540,8 +513,7 @@
{ timelineManager }
height={ timelineManager . viewportHeight }
timelineTopOffset={ timelineManager . topSectionHeight }
timelineBottomOffset={ bottomSectionHeight }
{ isInLeadOutSection }
timelineBottomOffset={ timelineManager . bottomSectionHeight }
{ timelineScrollPercent }
{ viewportTopMonthScrollPercent }
{ viewportTopMonth }
@ -580,7 +552,7 @@
bind:this={ timelineElement }
id="virtual-timeline"
class:invisible
style:height={ timelineManager . t imeline Height + 'px' }
style:height={ timelineManager . t otalViewer Height + 'px' }
>
< section
use:resizeObserver={ topSectionResizeObserver }
@ -636,11 +608,11 @@
{ /each }
<!-- spacer for leadout -->
< div
class="h-[60px]"
style:height={ timelineManager . bottomSectionHeight + 'px' }
style:position="absolute"
style:left="0"
style:right="0"
style:transform={ `translate3d(0, ${ timelineManager . t imelineHeight} px,0)` }
style:transform={ `translate3d(0, ${ timelineManager . t opSectionHeight + t imelineManager. assets Height} px,0)` }
>< / div >
< / section >
< / section >