다시 작업. 디자인도 바꾸고 서버도 바꿈. Co-authored-by: monoid <jaeung@prelude.duckdns.org> Reviewed-on: https://git.prelude.duckdns.org/monoid/ionian/pulls/6
166 lines
No EOL
6 KiB
TypeScript
166 lines
No EOL
6 KiB
TypeScript
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<HTMLImageElement>(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: <explanation>
|
|
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 (
|
|
<div className="overflow-hidden w-full h-full relative">
|
|
<div className="absolute left-0 w-1/2 h-full z-10" onMouseDown={() => PageDown(1)} />
|
|
<img
|
|
ref={currentImageRef}
|
|
className={cn("max-w-full max-h-full top-1/2 left-1/2 transform -translate-x-1/2 -translate-y-1/2 absolute",
|
|
fade ? "opacity-70 transition-opacity duration-300 ease-in-out" : "opacity-100"
|
|
)}
|
|
alt="main content"/>
|
|
<div className="absolute right-0 w-1/2 h-full z-10" onMouseDown={() => PageUp(1)} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
function clip(val: number, min: number, max: number): number {
|
|
return Math.max(min, Math.min(max, val));
|
|
}
|
|
|
|
function useFullScreen() {
|
|
const ref = useRef<HTMLElement>(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 <div className="p-4">
|
|
Loading...
|
|
</div>
|
|
}
|
|
if (error) {
|
|
return <div className="p-4">Error: {String(error)}</div>
|
|
}
|
|
if (!data) {
|
|
return <div className="p-4">Not found</div>
|
|
}
|
|
|
|
if (data.content_type !== "comic") {
|
|
return <div className="p-4">Not a comic</div>
|
|
}
|
|
if (!("page" in data.additional)) {
|
|
console.error(`invalid content : page read fail : ${JSON.stringify(data.additional)}`);
|
|
return <div className="p-4">Error. DB error. page restriction</div>
|
|
}
|
|
|
|
return (
|
|
<PageNavItem items={<>
|
|
<NavItem to={`/doc/${params.id}`} name="Back" icon={<ExitIcon />}/>
|
|
<NavItemButton name={isFullScreen ? "Exit Fullscreen" : "Enter Fullscreen"} icon={isFullScreen ? <ExitFullScreenIcon/> : <EnterFullScreenIcon/>} onClick={()=>{
|
|
toggleFullScreen();
|
|
}} />
|
|
<Popover>
|
|
<PopoverTrigger>
|
|
<span className="text-sm text-ellipsis" >{curPage + 1}/{data.additional.page as number}</span>
|
|
</PopoverTrigger>
|
|
<PopoverContent className="w-28">
|
|
<Input type="number" value={curPage + 1} onChange={(e) =>
|
|
setCurPage(clip(Number.parseInt(e.target.value) - 1,
|
|
0,
|
|
(data.additional.page as number) - 1))} />
|
|
</PopoverContent>
|
|
</Popover>
|
|
</>}>
|
|
<ComicViewer
|
|
curPage={curPage}
|
|
onChangePage={setCurPage}
|
|
doc={data}
|
|
totalPage={data.additional.page as number} />
|
|
</PageNavItem>
|
|
)
|
|
} |