import { createEffect, createSignal, onCleanup, untrack, } from "solid-js"; import type { GalleryPreviewCache } from "../../App/GalleryPreviewCache"; import type { GalleryCoordinator } from "../../App/GalleryCoordinator"; import * as eh from "../../eh"; import texts from "../../i18n"; import { createPointerGestureElement, type PointerDragEnd } from "../PointerGesture"; import { LoadingOverlay } from "../Widgets/Loading"; import { SwipeIndicator, type SwipeIndicatorState } from "../Widgets/SwipeIndicator"; import { GalleryPageDescription, ScrollPageBar } from "./ScrollPageBar"; const SWIPE_MIN_DISTANCE = 96; const SWIPE_INTENT_DISTANCE = 28; const HORIZONTAL_INTENT_RATIO = 2.2; const SWIPE_MAX_VERTICAL_RATIO = 0.38; export type ThumbsGridsActions = { gotoPreview: (previewIndex: number) => void; }; export function ThumbsGrids(props: { coordinator: GalleryCoordinator; onLoadError: (error: unknown) => void; previewCache: GalleryPreviewCache; }) { const pageBarSource = untrack(() => props.previewCache.current()); const [pageBarCurrentIndex, setPageBarCurrentIndex] = createSignal( pageBarSource.data.currentIndex, ); const [swipeIndicatorState, setSwipeIndicatorState] = createSignal({ blocked: false, direction: "left", progress: 0, }); const pageBarMaxIndex = (): number => props.previewCache.current().data.maxIndex; const requestPreviewPage = ( previewIndex: number, scrollToPageBar: "bottom" | "top", ): void => { const current = props.previewCache.current(); const onLoadError = props.onLoadError; pageBarSource.handle.scrollPreviewPageBarIntoView(scrollToPageBar); if (previewIndex === current.data.currentIndex) { return; } setPageBarCurrentIndex(previewIndex); void props.previewCache.select(previewIndex).then( (next) => { if (untrack(() => props.previewCache.current()) === next) { pageBarSource.handle.scrollPreviewPageBarIntoView(scrollToPageBar); } }, (error: unknown) => { const cacheIndex = untrack(() => props.previewCache.current().data.currentIndex); setPageBarCurrentIndex((currentIndex) => currentIndex === previewIndex ? cacheIndex : currentIndex, ); onLoadError(error); }, ); }; const swipeIndexForDelta = (dx: number): number | null => { const current = props.previewCache.current().data; const nextIndex = dx < 0 ? current.currentIndex + 1 : current.currentIndex - 1; if (nextIndex < 0 || nextIndex > current.maxIndex) { return null; } return nextIndex; }; const hideSwipeIndicator = (): void => { setSwipeIndicatorState((current) => ({ ...current, blocked: false, progress: 0 })); }; const updateSwipeIndicator = (info: PointerDragEnd): void => { setSwipeIndicatorState({ blocked: swipeIndexForDelta(info.dx) === null, direction: info.dx < 0 ? "left" : "right", progress: Math.min(1, Math.max(0, (Math.abs(info.dx) - SWIPE_INTENT_DISTANCE) / (SWIPE_MIN_DISTANCE - SWIPE_INTENT_DISTANCE))), }); }; const navigateBySwipe = (info: PointerDragEnd, event: Event): void => { const absX = Math.abs(info.dx); const absY = Math.abs(info.dy); if (absX < SWIPE_MIN_DISTANCE || absY > absX * SWIPE_MAX_VERTICAL_RATIO) { return; } const previewIndex = swipeIndexForDelta(info.dx); if (previewIndex !== null) { event.preventDefault(); requestPreviewPage(previewIndex, info.dx < 0 ? "top" : "bottom"); } }; const actions: ThumbsGridsActions = { gotoPreview: setPageBarCurrentIndex, }; untrack(() => props.coordinator).attachThumbs(actions); createEffect((previous) => { const current = props.previewCache.current(); setPageBarCurrentIndex(current.data.currentIndex); current.handle.ensurePreviewSwipeInput(); if (current !== previous) { pageBarSource.handle.replacePreviewThumbs(current.elems.thumbItems); } return current; }, pageBarSource); createEffect(() => { pageBarSource.handle.updatePreviewLoading(props.previewCache.loading()); }); onCleanup(() => { pageBarSource.handle.updatePreviewLoading(false); }); createPointerGestureElement( () => pageBarSource.elems.thumbs?.Component() ?? null, () => ({ onStart: hideSwipeIndicator, onMove: updateSwipeIndicator, onEnd: (info, event) => { navigateBySwipe(info, event); hideSwipeIndicator(); }, dragAxis: "x", dragIntentRatio: HORIZONTAL_INTENT_RATIO, dragStartThreshold: SWIPE_INTENT_DISTANCE, }), ); pageBarSource.handle.installPreviewPageBars(); pageBarSource.elems.pageBarDescription?.mount(() => ( )); for (const element of [ pageBarSource.elems.pageBarTop, pageBarSource.elems.pageBarBottom, ]) { if (!element) { continue; } element.mount(() => ( eh.previewUrlForIndex(index, props.previewCache.current().data.currentUrl) } /> )); } onCleanup(() => { pageBarSource.elems.pageBarDescription?.remove(); pageBarSource.elems.pageBarTop?.remove(); pageBarSource.elems.pageBarBottom?.remove(); }); return ( <> ); }