import { NavItem, NavItemButton } from "@/components/layout/nav"; import { PageNavItem } from "@/components/layout/navAtom"; import { Input } from "@/components/ui/input"; import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover"; import { useGalleryDoc } from "@/hook/useGalleryDoc.ts"; import { cn } from "@/lib/utils"; import { EnterFullScreenIcon, ExitFullScreenIcon, ExitIcon } from "@radix-ui/react-icons"; import { useEventListener } from "usehooks-ts"; import type { Document } from "dbtype/api"; import { useCallback, useEffect, useRef, useState } from "react"; interface ComicPageProps { params: { id: string; }; } function ComicViewer({ doc, totalPage, curPage, onChangePage: setCurPage, }: { doc: Document; totalPage: number; curPage: number; onChangePage: (page: number) => void; }) { const [fade, setFade] = useState(false); const PageDown = useCallback((step: number) => setCurPage(Math.max(curPage - step, 0)), [curPage, setCurPage]); const PageUp = useCallback((step: number) => setCurPage(Math.min(curPage + step, totalPage - 1)), [curPage, setCurPage, totalPage]); const currentImageRef = useRef(null); useEffect(() => { const onKeyUp = (e: KeyboardEvent) => { const step = e.shiftKey ? 10 : 1; if (e.code === "ArrowLeft") { PageDown(step); } else if (e.code === "ArrowRight") { PageUp(step); } }; window.addEventListener("keyup", onKeyUp); return () => { window.removeEventListener("keyup", onKeyUp); }; }, [PageDown, PageUp]); useEffect(() => { if(currentImageRef.current){ if (curPage < 0 || curPage >= totalPage) { return; } const img = new Image(); img.src = `/api/doc/${doc.id}/comic/${curPage}`; if (img.complete) { currentImageRef.current.src = img.src; setFade(false); return; } setFade(true); const listener = () => { // biome-ignore lint/style/noNonNullAssertion: const currentImage = currentImageRef.current!; currentImage.src = img.src; setFade(false); }; img.addEventListener("load", listener); return () => { img.removeEventListener("load", listener); // abort loading img.src = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEAAAAALAAAAAABAAEAAAI=;'; // TODO: use web worker to abort loading image in the future }; } }, [curPage, doc.id, totalPage]); return (
PageDown(1)} /> main content
PageUp(1)} />
); } function clip(val: number, min: number, max: number): number { return Math.max(min, Math.min(max, val)); } function useFullScreen() { const ref = useRef(document.documentElement); const [isFullScreen, setIsFullScreen] = useState(false); const toggleFullScreen = useCallback(() => { if (isFullScreen) { document.exitFullscreen(); } else { document.documentElement.requestFullscreen(); } }, [isFullScreen]); useEventListener("fullscreenchange", () => { setIsFullScreen(!!document.fullscreenElement); }, ref); return { isFullScreen, toggleFullScreen }; } export default function ComicPage({ params }: ComicPageProps) { const { data, error, isLoading } = useGalleryDoc(params.id); const [curPage, setCurPage] = useState(0); const { isFullScreen, toggleFullScreen } = useFullScreen(); if (isLoading) { // TODO: Add a loading spinner return
Loading...
} if (error) { return
Error: {String(error)}
} if (!data) { return
Not found
} if (data.content_type !== "comic") { return
Not a comic
} if (!("page" in data.additional)) { console.error(`invalid content : page read fail : ${JSON.stringify(data.additional)}`); return
Error. DB error. page restriction
} return ( }/> : } onClick={()=>{ toggleFullScreen(); }} /> {curPage + 1}/{data.additional.page as number} setCurPage(clip(Number.parseInt(e.target.value) - 1, 0, (data.additional.page as number) - 1))} /> }> ) }