mirror of
				https://github.com/immich-app/immich
				synced 2025-10-17 18:19:27 +00:00 
			
		
		
		
	refactor(web): Extract VirtualScrollManager base class from TimelineManager (#23017)
	
		
			
	
		
	
	
		
	
		
			Some checks failed
		
		
	
	
		
			
				
	
				CLI Build / CLI Publish (push) Has been cancelled
				
			
		
			
				
	
				CLI Build / Docker (push) Has been cancelled
				
			
		
			
				
	
				CodeQL / Analyze (push) Has been cancelled
				
			
		
			
				
	
				Docker / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag ML (push) Has been cancelled
				
			
		
			
				
	
				Docker / Re-Tag Server (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push ML (push) Has been cancelled
				
			
		
			
				
	
				Docker / Build and Push Server (push) Has been cancelled
				
			
		
			
				
	
				Docker / Docker Build & Push Server Success (push) Has been cancelled
				
			
		
			
				
	
				Docker / Docker Build & Push ML Success (push) Has been cancelled
				
			
		
			
				
	
				Docs build / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Docs build / Docs Build (push) Has been cancelled
				
			
		
			
				
	
				Zizmor / Zizmor (push) Has been cancelled
				
			
		
			
				
	
				Static Code Analysis / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Static Code Analysis / Run Dart Code Analysis (push) Has been cancelled
				
			
		
			
				
	
				Test / pre-job (push) Has been cancelled
				
			
		
			
				
	
				Test / Test & Lint Server (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test CLI (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test CLI (Windows) (push) Has been cancelled
				
			
		
			
				
	
				Test / Lint Web (push) Has been cancelled
				
			
		
			
				
	
				Test / Test Web (push) Has been cancelled
				
			
		
			
				
	
				Test / Test i18n (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Lint (push) Has been cancelled
				
			
		
			
				
	
				Test / Medium Tests (Server) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests (Server & CLI) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests (Web) (push) Has been cancelled
				
			
		
			
				
	
				Test / End-to-End Tests Success (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test Mobile (push) Has been cancelled
				
			
		
			
				
	
				Test / Unit Test ML (push) Has been cancelled
				
			
		
			
				
	
				Test / .github Files Formatting (push) Has been cancelled
				
			
		
			
				
	
				Test / ShellCheck (push) Has been cancelled
				
			
		
			
				
	
				Test / OpenAPI Clients (push) Has been cancelled
				
			
		
			
				
	
				Test / SQL Schema Checks (push) Has been cancelled
				
			
		
		
	
	
				
					
				
			
		
			Some checks failed
		
		
	
	CLI Build / CLI Publish (push) Has been cancelled
				
			CLI Build / Docker (push) Has been cancelled
				
			CodeQL / Analyze (push) Has been cancelled
				
			Docker / pre-job (push) Has been cancelled
				
			Docker / Re-Tag ML (push) Has been cancelled
				
			Docker / Re-Tag Server (push) Has been cancelled
				
			Docker / Build and Push ML (push) Has been cancelled
				
			Docker / Build and Push Server (push) Has been cancelled
				
			Docker / Docker Build & Push Server Success (push) Has been cancelled
				
			Docker / Docker Build & Push ML Success (push) Has been cancelled
				
			Docs build / pre-job (push) Has been cancelled
				
			Docs build / Docs Build (push) Has been cancelled
				
			Zizmor / Zizmor (push) Has been cancelled
				
			Static Code Analysis / pre-job (push) Has been cancelled
				
			Static Code Analysis / Run Dart Code Analysis (push) Has been cancelled
				
			Test / pre-job (push) Has been cancelled
				
			Test / Test & Lint Server (push) Has been cancelled
				
			Test / Unit Test CLI (push) Has been cancelled
				
			Test / Unit Test CLI (Windows) (push) Has been cancelled
				
			Test / Lint Web (push) Has been cancelled
				
			Test / Test Web (push) Has been cancelled
				
			Test / Test i18n (push) Has been cancelled
				
			Test / End-to-End Lint (push) Has been cancelled
				
			Test / Medium Tests (Server) (push) Has been cancelled
				
			Test / End-to-End Tests (Server & CLI) (push) Has been cancelled
				
			Test / End-to-End Tests (Web) (push) Has been cancelled
				
			Test / End-to-End Tests Success (push) Has been cancelled
				
			Test / Unit Test Mobile (push) Has been cancelled
				
			Test / Unit Test ML (push) Has been cancelled
				
			Test / .github Files Formatting (push) Has been cancelled
				
			Test / ShellCheck (push) Has been cancelled
				
			Test / OpenAPI Clients (push) Has been cancelled
				
			Test / SQL Schema Checks (push) Has been cancelled
				
			Extract common virtual scrolling functionality from TimelineManager into a new abstract VirtualScrollManager base class. This refactoring improves code organization and enables reuse of virtual scrolling logic. Changes: - Create new VirtualScrollManager abstract base class with common virtual scrolling state and methods - Refactor TimelineManager to extend VirtualScrollManager - Rename 'assetsHeight' to 'bodySectionHeight' for semantic clarity - Convert methods to use override keyword where appropriate - Enable noImplicitOverride in tsconfig for better type safety - Fix ApiError and AbortError class definitions with override keywords
This commit is contained in:
		
							parent
							
								
									e7d6a066f8
								
							
						
					
					
						commit
						3174a27902
					
				
					 6 changed files with 215 additions and 173 deletions
				
			
		| 
						 | 
				
			
			@ -250,7 +250,7 @@
 | 
			
		|||
      scrollToSegmentPercentage(0, timelineManager.topSectionHeight, scrubberMonthScrollPercent);
 | 
			
		||||
    } else if (leadOut) {
 | 
			
		||||
      scrollToSegmentPercentage(
 | 
			
		||||
        timelineManager.topSectionHeight + timelineManager.assetsHeight,
 | 
			
		||||
        timelineManager.topSectionHeight + timelineManager.bodySectionHeight,
 | 
			
		||||
        timelineManager.bottomSectionHeight,
 | 
			
		||||
        scrubberMonthScrollPercent,
 | 
			
		||||
      );
 | 
			
		||||
| 
						 | 
				
			
			@ -611,7 +611,7 @@
 | 
			
		|||
      style:position="absolute"
 | 
			
		||||
      style:left="0"
 | 
			
		||||
      style:right="0"
 | 
			
		||||
      style:transform={`translate3d(0,${timelineManager.topSectionHeight + timelineManager.assetsHeight}px,0)`}
 | 
			
		||||
      style:transform={`translate3d(0,${timelineManager.topSectionHeight + timelineManager.bodySectionHeight}px,0)`}
 | 
			
		||||
    ></div>
 | 
			
		||||
  </section>
 | 
			
		||||
</section>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -0,0 +1,168 @@
 | 
			
		|||
import { debounce } from 'lodash-es';
 | 
			
		||||
 | 
			
		||||
type LayoutOptions = {
 | 
			
		||||
  headerHeight: number;
 | 
			
		||||
  rowHeight: number;
 | 
			
		||||
  gap: number;
 | 
			
		||||
};
 | 
			
		||||
export abstract class VirtualScrollManager {
 | 
			
		||||
  topSectionHeight = $state(0);
 | 
			
		||||
  bodySectionHeight = $state(0);
 | 
			
		||||
  bottomSectionHeight = $state(0);
 | 
			
		||||
  totalViewerHeight = $derived.by(() => this.topSectionHeight + this.bodySectionHeight + this.bottomSectionHeight);
 | 
			
		||||
 | 
			
		||||
  visibleWindow = $derived.by(() => ({
 | 
			
		||||
    top: this.#scrollTop,
 | 
			
		||||
    bottom: this.#scrollTop + this.viewportHeight,
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  #viewportHeight = $state(0);
 | 
			
		||||
  #viewportWidth = $state(0);
 | 
			
		||||
  #scrollTop = $state(0);
 | 
			
		||||
  #rowHeight = $state(235);
 | 
			
		||||
  #headerHeight = $state(48);
 | 
			
		||||
  #gap = $state(12);
 | 
			
		||||
  #scrolling = $state(false);
 | 
			
		||||
  #suspendTransitions = $state(false);
 | 
			
		||||
  #resetScrolling = debounce(() => (this.#scrolling = false), 1000);
 | 
			
		||||
  #resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
 | 
			
		||||
  #justifiedLayoutOptions = $derived.by(() => ({
 | 
			
		||||
    spacing: 2,
 | 
			
		||||
    heightTolerance: 0.15,
 | 
			
		||||
    rowHeight: this.#rowHeight,
 | 
			
		||||
    rowWidth: Math.floor(this.viewportWidth),
 | 
			
		||||
  }));
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    this.setLayoutOptions();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get scrollTop() {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get justifiedLayoutOptions() {
 | 
			
		||||
    return this.#justifiedLayoutOptions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get maxScrollPercent() {
 | 
			
		||||
    const totalHeight = this.totalViewerHeight;
 | 
			
		||||
    return (totalHeight - this.viewportHeight) / totalHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get maxScroll() {
 | 
			
		||||
    return this.totalViewerHeight - this.viewportHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #setHeaderHeight(value: number) {
 | 
			
		||||
    if (this.#headerHeight == value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    this.#headerHeight = value;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get headerHeight() {
 | 
			
		||||
    return this.#headerHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #setGap(value: number) {
 | 
			
		||||
    if (this.#gap == value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    this.#gap = value;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get gap() {
 | 
			
		||||
    return this.#gap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #setRowHeight(value: number) {
 | 
			
		||||
    if (this.#rowHeight == value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    this.#rowHeight = value;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get rowHeight() {
 | 
			
		||||
    return this.#rowHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set scrolling(value: boolean) {
 | 
			
		||||
    this.#scrolling = value;
 | 
			
		||||
    if (value) {
 | 
			
		||||
      this.suspendTransitions = true;
 | 
			
		||||
      this.#resetScrolling();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get scrolling() {
 | 
			
		||||
    return this.#scrolling;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set suspendTransitions(value: boolean) {
 | 
			
		||||
    this.#suspendTransitions = value;
 | 
			
		||||
    if (value) {
 | 
			
		||||
      this.#resetSuspendTransitions();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get suspendTransitions() {
 | 
			
		||||
    return this.#suspendTransitions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set viewportWidth(value: number) {
 | 
			
		||||
    const changed = value !== this.#viewportWidth;
 | 
			
		||||
    this.#viewportWidth = value;
 | 
			
		||||
    this.suspendTransitions = true;
 | 
			
		||||
    void this.updateViewportGeometry(changed);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get viewportWidth() {
 | 
			
		||||
    return this.#viewportWidth;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set viewportHeight(value: number) {
 | 
			
		||||
    this.#viewportHeight = value;
 | 
			
		||||
    this.#suspendTransitions = true;
 | 
			
		||||
    void this.updateViewportGeometry(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get viewportHeight() {
 | 
			
		||||
    return this.#viewportHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get hasEmptyViewport() {
 | 
			
		||||
    return this.viewportWidth === 0 || this.viewportHeight === 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updateIntersections(): void {}
 | 
			
		||||
 | 
			
		||||
  protected updateViewportGeometry(_: boolean) {}
 | 
			
		||||
 | 
			
		||||
  setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: Partial<LayoutOptions> = {}) {
 | 
			
		||||
    let changed = false;
 | 
			
		||||
    changed ||= this.#setHeaderHeight(headerHeight);
 | 
			
		||||
    changed ||= this.#setGap(gap);
 | 
			
		||||
    changed ||= this.#setRowHeight(rowHeight);
 | 
			
		||||
    if (changed) {
 | 
			
		||||
      this.refreshLayout();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateSlidingWindow() {
 | 
			
		||||
    const scrollTop = this.scrollTop;
 | 
			
		||||
    if (this.#scrollTop !== scrollTop) {
 | 
			
		||||
      this.#scrollTop = scrollTop;
 | 
			
		||||
      this.updateIntersections();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  refreshLayout() {
 | 
			
		||||
    this.updateIntersections();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  destroy(): void {}
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			@ -28,7 +28,7 @@ export function layoutMonthGroup(timelineManager: TimelineManager, month: MonthG
 | 
			
		|||
  let dayGroupRow = 0;
 | 
			
		||||
  let dayGroupCol = 0;
 | 
			
		||||
 | 
			
		||||
  const options = timelineManager.createLayoutOptions();
 | 
			
		||||
  const options = timelineManager.justifiedLayoutOptions;
 | 
			
		||||
  for (const dayGroup of month.dayGroups) {
 | 
			
		||||
    dayGroup.layout(options, noDefer);
 | 
			
		||||
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1,13 +1,5 @@
 | 
			
		|||
import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
 | 
			
		||||
 | 
			
		||||
import { VirtualScrollManager } from '$lib/managers/VirtualScrollManager/VirtualScrollManager.svelte';
 | 
			
		||||
import { authManager } from '$lib/managers/auth-manager.svelte';
 | 
			
		||||
 | 
			
		||||
import { CancellableTask } from '$lib/utils/cancellable-task';
 | 
			
		||||
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
 | 
			
		||||
 | 
			
		||||
import { clamp, debounce, isEqual } from 'lodash-es';
 | 
			
		||||
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
 | 
			
		||||
 | 
			
		||||
import { updateIntersectionMonthGroup } from '$lib/managers/timeline-manager/internal/intersection-support.svelte';
 | 
			
		||||
import { updateGeometry } from '$lib/managers/timeline-manager/internal/layout-support.svelte';
 | 
			
		||||
import { loadFromTimeBuckets } from '$lib/managers/timeline-manager/internal/load-support.svelte';
 | 
			
		||||
| 
						 | 
				
			
			@ -24,6 +16,11 @@ import {
 | 
			
		|||
  retrieveRange as retrieveRangeUtil,
 | 
			
		||||
} from '$lib/managers/timeline-manager/internal/search-support.svelte';
 | 
			
		||||
import { WebsocketSupport } from '$lib/managers/timeline-manager/internal/websocket-support.svelte';
 | 
			
		||||
import { CancellableTask } from '$lib/utils/cancellable-task';
 | 
			
		||||
import { toTimelineAsset, type TimelineDateTime, type TimelineYearMonth } from '$lib/utils/timeline-util';
 | 
			
		||||
import { AssetOrder, getAssetInfo, getTimeBuckets } from '@immich/sdk';
 | 
			
		||||
import { clamp, isEqual } from 'lodash-es';
 | 
			
		||||
import { SvelteDate, SvelteMap, SvelteSet } from 'svelte/reactivity';
 | 
			
		||||
import { DayGroup } from './day-group.svelte';
 | 
			
		||||
import { isMismatched, updateObject } from './internal/utils.svelte';
 | 
			
		||||
import { MonthGroup } from './month-group.svelte';
 | 
			
		||||
| 
						 | 
				
			
			@ -33,7 +30,6 @@ import type {
 | 
			
		|||
  Direction,
 | 
			
		||||
  ScrubberMonth,
 | 
			
		||||
  TimelineAsset,
 | 
			
		||||
  TimelineManagerLayoutOptions,
 | 
			
		||||
  TimelineManagerOptions,
 | 
			
		||||
  Viewport,
 | 
			
		||||
} from './types';
 | 
			
		||||
| 
						 | 
				
			
			@ -45,28 +41,32 @@ type ViewportTopMonthIntersection = {
 | 
			
		|||
  // Where month bottom is in viewport (0 = viewport top, 1 = viewport bottom)
 | 
			
		||||
  monthBottomViewportRatio: number;
 | 
			
		||||
};
 | 
			
		||||
export class TimelineManager {
 | 
			
		||||
export class TimelineManager extends VirtualScrollManager {
 | 
			
		||||
  override bottomSectionHeight = $state(60);
 | 
			
		||||
 | 
			
		||||
  override bodySectionHeight = $derived.by(() => {
 | 
			
		||||
    let height = 0;
 | 
			
		||||
    for (const month of this.months) {
 | 
			
		||||
      height += month.height;
 | 
			
		||||
    }
 | 
			
		||||
    return height;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  assetCount = $derived.by(() => {
 | 
			
		||||
    let count = 0;
 | 
			
		||||
    for (const month of this.months) {
 | 
			
		||||
      count += month.assetsCount;
 | 
			
		||||
    }
 | 
			
		||||
    return count;
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  isInitialized = $state(false);
 | 
			
		||||
  months: MonthGroup[] = $state([]);
 | 
			
		||||
  topSectionHeight = $state(0);
 | 
			
		||||
  bottomSectionHeight = $state(60);
 | 
			
		||||
  assetsHeight = $derived(this.months.reduce((accumulator, b) => accumulator + b.height, 0));
 | 
			
		||||
  totalViewerHeight = $derived(this.topSectionHeight + this.assetsHeight + this.bottomSectionHeight);
 | 
			
		||||
  assetCount = $derived(this.months.reduce((accumulator, b) => accumulator + b.assetsCount, 0));
 | 
			
		||||
 | 
			
		||||
  albumAssets: Set<string> = new SvelteSet();
 | 
			
		||||
 | 
			
		||||
  scrubberMonths: ScrubberMonth[] = $state([]);
 | 
			
		||||
  scrubberTimelineHeight: number = $state(0);
 | 
			
		||||
 | 
			
		||||
  viewportTopMonthIntersection: ViewportTopMonthIntersection | undefined;
 | 
			
		||||
 | 
			
		||||
  visibleWindow = $derived.by(() => ({
 | 
			
		||||
    top: this.#scrollTop,
 | 
			
		||||
    bottom: this.#scrollTop + this.viewportHeight,
 | 
			
		||||
  }));
 | 
			
		||||
  limitedScroll = $derived(this.maxScrollPercent < 0.5);
 | 
			
		||||
 | 
			
		||||
  initTask = new CancellableTask(
 | 
			
		||||
    () => {
 | 
			
		||||
      this.isInitialized = true;
 | 
			
		||||
| 
						 | 
				
			
			@ -83,32 +83,17 @@ export class TimelineManager {
 | 
			
		|||
  );
 | 
			
		||||
 | 
			
		||||
  static #INIT_OPTIONS = {};
 | 
			
		||||
  #viewportHeight = $state(0);
 | 
			
		||||
  #viewportWidth = $state(0);
 | 
			
		||||
  #scrollTop = $state(0);
 | 
			
		||||
  #websocketSupport: WebsocketSupport | undefined;
 | 
			
		||||
 | 
			
		||||
  #rowHeight = $state(235);
 | 
			
		||||
  #headerHeight = $state(48);
 | 
			
		||||
  #gap = $state(12);
 | 
			
		||||
 | 
			
		||||
  #options: TimelineManagerOptions = TimelineManager.#INIT_OPTIONS;
 | 
			
		||||
 | 
			
		||||
  #scrolling = $state(false);
 | 
			
		||||
  #suspendTransitions = $state(false);
 | 
			
		||||
  #resetScrolling = debounce(() => (this.#scrolling = false), 1000);
 | 
			
		||||
  #resetSuspendTransitions = debounce(() => (this.suspendTransitions = false), 1000);
 | 
			
		||||
  #updatingIntersections = false;
 | 
			
		||||
  #scrollableElement: HTMLElement | undefined = $state();
 | 
			
		||||
 | 
			
		||||
  setLayoutOptions({ headerHeight = 48, rowHeight = 235, gap = 12 }: TimelineManagerLayoutOptions) {
 | 
			
		||||
    let changed = false;
 | 
			
		||||
    changed ||= this.#setHeaderHeight(headerHeight);
 | 
			
		||||
    changed ||= this.#setGap(gap);
 | 
			
		||||
    changed ||= this.#setRowHeight(rowHeight);
 | 
			
		||||
    if (changed) {
 | 
			
		||||
      this.refreshLayout();
 | 
			
		||||
    }
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  override get scrollTop(): number {
 | 
			
		||||
    return this.#scrollableElement?.scrollTop ?? 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set scrollableElement(element: HTMLElement | undefined) {
 | 
			
		||||
| 
						 | 
				
			
			@ -125,87 +110,6 @@ export class TimelineManager {
 | 
			
		|||
    this.updateSlidingWindow();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #setHeaderHeight(value: number) {
 | 
			
		||||
    if (this.#headerHeight == value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    this.#headerHeight = value;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get headerHeight() {
 | 
			
		||||
    return this.#headerHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #setGap(value: number) {
 | 
			
		||||
    if (this.#gap == value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    this.#gap = value;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get gap() {
 | 
			
		||||
    return this.#gap;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #setRowHeight(value: number) {
 | 
			
		||||
    if (this.#rowHeight == value) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    this.#rowHeight = value;
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get rowHeight() {
 | 
			
		||||
    return this.#rowHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set scrolling(value: boolean) {
 | 
			
		||||
    this.#scrolling = value;
 | 
			
		||||
    if (value) {
 | 
			
		||||
      this.suspendTransitions = true;
 | 
			
		||||
      this.#resetScrolling();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get scrolling() {
 | 
			
		||||
    return this.#scrolling;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set suspendTransitions(value: boolean) {
 | 
			
		||||
    this.#suspendTransitions = value;
 | 
			
		||||
    if (value) {
 | 
			
		||||
      this.#resetSuspendTransitions();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get suspendTransitions() {
 | 
			
		||||
    return this.#suspendTransitions;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set viewportWidth(value: number) {
 | 
			
		||||
    const changed = value !== this.#viewportWidth;
 | 
			
		||||
    this.#viewportWidth = value;
 | 
			
		||||
    this.suspendTransitions = true;
 | 
			
		||||
    this.#updateViewportGeometry(changed);
 | 
			
		||||
    this.updateSlidingWindow();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get viewportWidth() {
 | 
			
		||||
    return this.#viewportWidth;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  set viewportHeight(value: number) {
 | 
			
		||||
    this.#viewportHeight = value;
 | 
			
		||||
    this.#suspendTransitions = true;
 | 
			
		||||
    void this.#updateViewportGeometry(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get viewportHeight() {
 | 
			
		||||
    return this.#viewportHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async *assetsIterator(options?: {
 | 
			
		||||
    startMonthGroup?: MonthGroup;
 | 
			
		||||
    startDayGroup?: DayGroup;
 | 
			
		||||
| 
						 | 
				
			
			@ -251,14 +155,6 @@ export class TimelineManager {
 | 
			
		|||
    this.#websocketSupport = undefined;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateSlidingWindow() {
 | 
			
		||||
    const scrollTop = this.#scrollableElement?.scrollTop ?? 0;
 | 
			
		||||
    if (this.#scrollTop !== scrollTop) {
 | 
			
		||||
      this.#scrollTop = scrollTop;
 | 
			
		||||
      this.updateIntersections();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #calculateMonthBottomViewportRatio(month: MonthGroup | undefined) {
 | 
			
		||||
    if (!month) {
 | 
			
		||||
      return 0;
 | 
			
		||||
| 
						 | 
				
			
			@ -276,7 +172,7 @@ export class TimelineManager {
 | 
			
		|||
    return clamp((this.visibleWindow.top - month.top) / month.height, 0, 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  updateIntersections() {
 | 
			
		||||
  override updateIntersections() {
 | 
			
		||||
    if (this.#updatingIntersections || !this.isInitialized || this.visibleWindow.bottom === this.visibleWindow.top) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			@ -325,7 +221,7 @@ export class TimelineManager {
 | 
			
		|||
      );
 | 
			
		||||
    });
 | 
			
		||||
    this.albumAssets.clear();
 | 
			
		||||
    this.#updateViewportGeometry(false);
 | 
			
		||||
    this.updateViewportGeometry(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateOptions(options: TimelineManagerOptions) {
 | 
			
		||||
| 
						 | 
				
			
			@ -337,7 +233,7 @@ export class TimelineManager {
 | 
			
		|||
    }
 | 
			
		||||
    await this.initTask.reset();
 | 
			
		||||
    await this.#init(options);
 | 
			
		||||
    this.#updateViewportGeometry(false);
 | 
			
		||||
    this.updateViewportGeometry(false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async #init(options: TimelineManagerOptions) {
 | 
			
		||||
| 
						 | 
				
			
			@ -350,9 +246,10 @@ export class TimelineManager {
 | 
			
		|||
    }, true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public destroy() {
 | 
			
		||||
  public override destroy() {
 | 
			
		||||
    this.disconnect();
 | 
			
		||||
    this.isInitialized = false;
 | 
			
		||||
    super.destroy();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async updateViewport(viewport: Viewport) {
 | 
			
		||||
| 
						 | 
				
			
			@ -371,14 +268,11 @@ export class TimelineManager {
 | 
			
		|||
    const changedWidth = viewport.width !== this.viewportWidth;
 | 
			
		||||
    this.viewportHeight = viewport.height;
 | 
			
		||||
    this.viewportWidth = viewport.width;
 | 
			
		||||
    this.#updateViewportGeometry(changedWidth);
 | 
			
		||||
    this.updateViewportGeometry(changedWidth);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  #updateViewportGeometry(changedWidth: boolean) {
 | 
			
		||||
    if (!this.isInitialized) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.viewportWidth === 0 || this.viewportHeight === 0) {
 | 
			
		||||
  protected override updateViewportGeometry(changedWidth: boolean) {
 | 
			
		||||
    if (!this.isInitialized || this.hasEmptyViewport) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    for (const month of this.months) {
 | 
			
		||||
| 
						 | 
				
			
			@ -401,27 +295,6 @@ export class TimelineManager {
 | 
			
		|||
    this.scrubberTimelineHeight = this.totalViewerHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  createLayoutOptions() {
 | 
			
		||||
    const viewportWidth = this.viewportWidth;
 | 
			
		||||
 | 
			
		||||
    return {
 | 
			
		||||
      spacing: 2,
 | 
			
		||||
      heightTolerance: 0.15,
 | 
			
		||||
      rowHeight: this.#rowHeight,
 | 
			
		||||
      rowWidth: Math.floor(viewportWidth),
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get maxScrollPercent() {
 | 
			
		||||
    const totalHeight = this.totalViewerHeight;
 | 
			
		||||
    const max = (totalHeight - this.viewportHeight) / totalHeight;
 | 
			
		||||
    return max;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  get maxScroll() {
 | 
			
		||||
    return this.totalViewerHeight - this.viewportHeight;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async loadMonthGroup(yearMonth: TimelineYearMonth, options?: { cancelable: boolean }): Promise<void> {
 | 
			
		||||
    let cancelable = true;
 | 
			
		||||
    if (options) {
 | 
			
		||||
| 
						 | 
				
			
			@ -559,7 +432,7 @@ export class TimelineManager {
 | 
			
		|||
    return [...unprocessedIds];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  refreshLayout() {
 | 
			
		||||
  override refreshLayout() {
 | 
			
		||||
    for (const month of this.months) {
 | 
			
		||||
      updateGeometry(this, month, { invalidateHeight: true });
 | 
			
		||||
    }
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -60,14 +60,14 @@ interface UploadRequestOptions {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
export class AbortError extends Error {
 | 
			
		||||
  name = 'AbortError';
 | 
			
		||||
  override name = 'AbortError';
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
class ApiError extends Error {
 | 
			
		||||
  name = 'ApiError';
 | 
			
		||||
  override name = 'ApiError';
 | 
			
		||||
 | 
			
		||||
  constructor(
 | 
			
		||||
    public message: string,
 | 
			
		||||
    public override message: string,
 | 
			
		||||
    public statusCode: number,
 | 
			
		||||
    public details: string,
 | 
			
		||||
  ) {
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -6,6 +6,7 @@
 | 
			
		|||
    "forceConsistentCasingInFileNames": true,
 | 
			
		||||
    "module": "es2022",
 | 
			
		||||
    "moduleResolution": "bundler",
 | 
			
		||||
    "noImplicitOverride": true,
 | 
			
		||||
    "resolveJsonModule": true,
 | 
			
		||||
    "skipLibCheck": true,
 | 
			
		||||
    "sourceMap": true,
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue