import { createEffect, createSignal, For, onCleanup, onMount, Show } from "solid-js"; import { state, type NavigationMode, type PageLayout, type ReadDirection, type RightTapAction, } from "../../state"; import texts from "../../i18n"; import { stopEvent } from "../../utils"; import { Dialog } from "../Widgets/Dialog"; import { Icon } from "../Widgets/Icon"; import { ProgressBar } from "../Widgets/ProgressBar"; import { InteractionHelp } from "../InteractionHelp"; export type ReaderControls = { navigationMode: NavigationMode; direction: ReadDirection; firstPageSeparate: boolean; pageLayout: PageLayout; rightTapAction: RightTapAction; }; export type PageProgress = { pageNum: number; totalPages?: number; maxProgressPageNum: number; keepInputValue?: boolean; }; export const READER_BUTTON_CLASS = [ "inline-flex ui-hit-min-w-md ui-hit-h-md items-center justify-center ui-px-md py-0 ui-rounded-md", "border border-[var(--color-border)] bg-[var(--color-control)] text-[var(--color-text)] cursor-pointer font-sans textsize-md font-700 leading-1 disabled:(opacity-40 cursor-default)", ].join(" "); const READER_TOOLBAR_BUTTON_CLASS = `${READER_BUTTON_CLASS} !w-[var(--ui-control-size-lg)] !min-w-0 !ui-px-sm flex-none`; export const READER_FLOATING_ACTION_CLASS = [ READER_BUTTON_CLASS, "!min-w-[var(--ui-control-size-lg)] !h-[var(--ui-control-size-lg)] opacity-85 hover:opacity-100 focus-visible:opacity-100 transition-opacity duration-160", ].join(" "); const READER_FLOATING_ICON_ACTION_CLASS = `${READER_FLOATING_ACTION_CLASS} !w-[calc(var(--ui-control-size-lg)*2)] px-0`; const READER_ICON_SIZE = "var(--ui-icon-size-md)"; const TIME_FORMATTER = new Intl.DateTimeFormat(undefined, { hour: "2-digit", minute: "2-digit", }); const DOWNLOAD_OPTION_CLASS = [ "flex w-full ui-hit-min-h-lg flex-col items-start justify-center ui-gap-xs ui-px-lg ui-py-md ui-rounded-md", "border border-[var(--color-border)] bg-[var(--color-control)] text-[var(--color-text)] cursor-pointer text-left", "hover:bg-[var(--color-badge)] disabled:(opacity-40 cursor-default)", ].join(" "); export type ReaderDownloadInfo = { currentFileName: string; currentImageUrl: string; imageHeight: number | null; imageWidth: number | null; originalFileName: string; originalImageUrl: string | null; pageNum: number; }; export type ToolbarCallbacks = { onCloseClick: () => void; onControlsChange: (controls: ReaderControls) => void; onFullscreenClick: () => void; onOpenOriginalPageClick: () => void; onOpenScrollPreviewClick: () => void; onProgressCommit: (value: number) => void; onProgressInput: (value: number) => void; onProgressPointerDown: (event: PointerEvent) => void; onViewportAdjustClick: () => void; }; export function Toolbar(props: { callbacks: ToolbarCallbacks; controls: ReaderControls; downloadInfos: ReaderDownloadInfo[]; fullscreenActive: boolean; open: boolean; progress: PageProgress; }) { const leftHandedControls = state.app.leftHandedControls.value; const [downloadDialogPageNum, setDownloadDialogPageNum] = createSignal(null); const [helpOpen, setHelpOpen] = createSignal(false); const [moreOpen, setMoreOpen] = createSignal(false); const [controlChange, setControlChange] = createSignal(null); const [fullscreenToolbarTop, setFullscreenToolbarTop] = createSignal(); let pageNumber!: HTMLDivElement; let fullscreenStatus: HTMLDivElement | undefined; let controlChangeTimer: number | null = null; const fullscreenTime = createFullscreenTime(() => props.fullscreenActive); const showControlChange = (message: string) => { if (controlChangeTimer !== null) { window.clearTimeout(controlChangeTimer); } setControlChange(message); controlChangeTimer = window.setTimeout(() => { setControlChange(null); controlChangeTimer = null; }, 1_200); }; onCleanup(() => { if (controlChangeTimer !== null) { window.clearTimeout(controlChangeTimer); } }); onMount(() => { const updateFullscreenToolbarTop = () => { if (!props.fullscreenActive) { setFullscreenToolbarTop(undefined); return; } const statusBottom = fullscreenStatus?.getBoundingClientRect().bottom ?? 0; const pageNumberBottom = pageNumber.getBoundingClientRect().bottom; setFullscreenToolbarTop(`${Math.ceil(Math.max(statusBottom, pageNumberBottom) + 8)}px`); }; const observer = new ResizeObserver(updateFullscreenToolbarTop); observer.observe(pageNumber); window.addEventListener("resize", updateFullscreenToolbarTop); createEffect(() => { if (props.fullscreenActive) { queueMicrotask(updateFullscreenToolbarTop); } else { updateFullscreenToolbarTop(); } }); onCleanup(() => { observer.disconnect(); window.removeEventListener("resize", updateFullscreenToolbarTop); }); }); createEffect(() => { if (!props.open) { setMoreOpen(false); } }); createEffect(() => { const pageNum = downloadDialogPageNum(); if (pageNum !== null && pageNum !== props.progress.pageNum) { setDownloadDialogPageNum(null); } }); createEffect(() => { if (downloadDialogPageNum() === null) { return; } const closeOnEscape = (event: KeyboardEvent) => { if (event.key !== "Escape") { return; } event.preventDefault(); event.stopPropagation(); setDownloadDialogPageNum(null); }; window.addEventListener("keydown", closeOnEscape, true); onCleanup(() => window.removeEventListener("keydown", closeOnEscape, true)); }); return (
{fullscreenTime()}
{(message) => (
{message}
)}
0}> setDownloadDialogPageNum(null)} title={`${texts.reader.download} · ${props.downloadInfos.map((info) => info.pageNum).join(", ")}`} variant="reader" width="lg" >
{(downloadInfo) => (
)}
{texts.reader.downloadHelpLabel}

{texts.reader.downloadHelp}

setHelpOpen(false)} />
); } function startImageDownload(url: string, name?: string): boolean { try { GM_download({ url, ...(name ? { name } : {}), onerror: (error) => { console.error("[ehpeek]", error); window.alert(texts.errors.downloadFailed); }, }); return true; } catch (error) { console.error("[ehpeek]", error); window.alert(texts.errors.downloadFailed); return false; } } function createFullscreenTime(enabled: () => boolean): () => string { const [time, setTime] = createSignal(TIME_FORMATTER.format(new Date())); createEffect(() => { if (!enabled()) { return; } const updateTime = () => setTime(TIME_FORMATTER.format(new Date())); updateTime(); let interval: number | null = null; const timeout = window.setTimeout(() => { updateTime(); interval = window.setInterval(updateTime, 60_000); }, 60_000 - (Date.now() % 60_000)); onCleanup(() => { window.clearTimeout(timeout); if (interval !== null) { window.clearInterval(interval); } }); }); return time; } function progressFillPercent(progress: PageProgress): number { const min = 1; const max = Math.max(1, progress.maxProgressPageNum); const value = Math.min(max, Math.max(min, progress.pageNum)); return max > min ? ((value - min) / (max - min)) * 100 : 100; } function pageNumberText( pageNum: number, totalPages: number | undefined, navigationMode: NavigationMode, pageLayout: PageLayout, firstPageSeparate: boolean, ): string { if (totalPages && pageNum === totalPages + 1) { return texts.reader.endPage; } const doublePage = navigationMode === "paged" && pageLayout === "double" && !(firstPageSeparate && pageNum === 1); if (!totalPages) { return doublePage ? `${pageNum}–${pageNum + 1}` : String(pageNum); } const doublePageEnd = Math.min(totalPages, pageNum + 1); return doublePage && doublePageEnd > pageNum ? `${pageNum}–${doublePageEnd} / ${totalPages}` : `${pageNum} / ${totalPages}`; }