diff --git a/packages/client/src/App.tsx b/packages/client/src/App.tsx index b8892e2..a51f7e4 100644 --- a/packages/client/src/App.tsx +++ b/packages/client/src/App.tsx @@ -15,14 +15,15 @@ import './App.css' // } from "./page/mod"; import { TooltipProvider } from "./components/ui/tooltip.tsx"; - -import Gallery from "./page/galleryPage.tsx"; import Layout from "./components/layout/layout.tsx"; -import NotFoundPage from "./page/404.tsx"; -import LoginPage from "./page/loginPage.tsx"; -import ProfilePage from "./page/profilesPage.tsx"; -import ContentInfoPage from "./page/contentInfoPage.tsx"; -import SettingPage from "./page/settingPage.tsx"; + +import Gallery from "@/page/galleryPage.tsx"; +import NotFoundPage from "@/page/404.tsx"; +import LoginPage from "@/page/loginPage.tsx"; +import ProfilePage from "@/page/profilesPage.tsx"; +import ContentInfoPage from "@/page/contentInfoPage.tsx"; +import SettingPage from "@/page/settingPage.tsx"; +import ComicPage from "@/page/reader/comicPage.tsx"; const App = () => { return ( @@ -35,10 +36,10 @@ const App = () => { + {/* - }> - }> - }>*/} + }/> + */} diff --git a/packages/client/src/component/contentinfo.tsx b/packages/client/src/component/contentinfo.tsx deleted file mode 100644 index c2497c3..0000000 --- a/packages/client/src/component/contentinfo.tsx +++ /dev/null @@ -1,238 +0,0 @@ -import React, {} from "react"; -import { Link as RouterLink } from "react-router-dom"; -import { Document } from "../accessor/document"; - -import { Box, Button, Grid, Link, Paper, Theme, Typography, useTheme } from "@mui/material"; -import { TagChip } from "../component/tagchip"; -import { ThumbnailContainer } from "../page/reader/reader"; - -import DocumentAccessor from "../accessor/document"; - -export const makeContentInfoUrl = (id: number) => `/doc/${id}`; -export const makeContentReaderUrl = (id: number) => `/doc/${id}/reader`; - -const useStyles = (theme: Theme) => ({ - thumbnail_content: { - maxHeight: "400px", - maxWidth: "min(400px, 100vw)", - }, - tag_list: { - display: "flex", - justifyContent: "flex-start", - flexWrap: "wrap", - overflowY: "hidden", - "& > *": { - margin: theme.spacing(0.5), - }, - }, - title: { - marginLeft: theme.spacing(1), - }, - infoContainer: { - padding: theme.spacing(2), - }, - subinfoContainer: { - display: "grid", - gridTemplateColumns: "100px auto", - overflowY: "hidden", - alignItems: "baseline", - }, - short_subinfoContainer: { - [theme.breakpoints.down("md")]: { - display: "none", - }, - }, - short_root: { - overflowY: "hidden", - display: "flex", - flexDirection: "column", - [theme.breakpoints.up("sm")]: { - height: 200, - flexDirection: "row", - }, - }, - short_thumbnail_anchor: { - background: "#272733", - display: "flex", - alignItems: "center", - justifyContent: "center", - [theme.breakpoints.up("sm")]: { - width: theme.spacing(25), - height: theme.spacing(25), - flexShrink: 0, - }, - }, - short_thumbnail_content: { - maxWidth: "100%", - maxHeight: "100%", - }, -}); - -export const ContentInfo = (props: { - document: Document; - children?: React.ReactNode; - classes?: { - root?: string; - thumbnail_anchor?: string; - thumbnail_content?: string; - tag_list?: string; - title?: string; - infoContainer?: string; - subinfoContainer?: string; - }; - gallery?: string; - short?: boolean; -}) => { - const theme = useTheme(); - const document = props.document; - const url = props.gallery === undefined ? makeContentReaderUrl(document.id) : makeContentInfoUrl(document.id); - return ( - - - {document.deleted_at === null ? ( - - ) : ( - Deleted - )} - - - - {document.title} - - - {props.short ? ( - - {document.tags.map((x) => ( - - ))} - - ) : ( - - )} - - {document.deleted_at != null && ( - - )} - - - ); -}; -async function documentDelete(id: number) { - const t = await DocumentAccessor.del(id); - if (t) { - alert("document deleted!"); - } else { - alert("document already deleted."); - } -} - -function ComicDetailTag(prop: { - tags: string[] /*classes:{ - tag_list:string -}*/; - path?: string; - createdAt?: number; - deletedAt?: number; -}) { - let allTag = prop.tags; - const tagKind = ["artist", "group", "series", "type", "character"]; - let tagTable: { [kind: string]: string[] } = {}; - for (const kind of tagKind) { - const tags = allTag.filter((x) => x.startsWith(kind + ":")).map((x) => x.slice(kind.length + 1)); - tagTable[kind] = tags; - allTag = allTag.filter((x) => !x.startsWith(kind + ":")); - } - return ( - - {tagKind.map((key) => ( - - - {key} - - - - {tagTable[key].length !== 0 - ? tagTable[key].map((elem, i) => { - return ( - <> - - {elem} - - {i < tagTable[key].length - 1 ? "," : ""} - - ); - }) - : "N/A"} - - - - ))} - {prop.path != undefined && ( - <> - - Path - - - {prop.path} - - - )} - {prop.createdAt != undefined && ( - <> - - CreatedAt - - - {new Date(prop.createdAt).toUTCString()} - - - )} - {prop.deletedAt != undefined && ( - <> - - DeletedAt - - - {new Date(prop.deletedAt).toUTCString()} - - - )} - - Tags - - - {allTag.map((x) => ( - - ))} - - - ); -} diff --git a/packages/client/src/component/mod.ts b/packages/client/src/component/mod.ts deleted file mode 100644 index a92d73a..0000000 --- a/packages/client/src/component/mod.ts +++ /dev/null @@ -1,5 +0,0 @@ -export * from "./contentinfo"; -export * from "./headline"; -export * from "./loading"; -export * from "./navlist"; -export * from "./tagchip"; diff --git a/packages/client/src/components/gallery/GalleryCard.tsx b/packages/client/src/components/gallery/GalleryCard.tsx index 064b32f..7a4e0f7 100644 --- a/packages/client/src/components/gallery/GalleryCard.tsx +++ b/packages/client/src/components/gallery/GalleryCard.tsx @@ -1,8 +1,7 @@ import type { Document } from "dbtype/api"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"; import TagBadge from "@/components/gallery/TagBadge.tsx"; -import { Fragment, ReactNode, useEffect, useRef, useState } from "react"; -import { Link, useLocation } from "wouter"; +import { Fragment, useEffect, useRef, useState } from "react"; import { LazyImage } from "./LazyImage.tsx"; import StyledLink from "./StyledLink.tsx"; diff --git a/packages/client/src/page/contentInfoPage.tsx b/packages/client/src/page/contentInfoPage.tsx index b45a949..39092a2 100644 --- a/packages/client/src/page/contentInfoPage.tsx +++ b/packages/client/src/page/contentInfoPage.tsx @@ -3,6 +3,7 @@ import { useGalleryDoc } from "../hook/useGalleryDoc"; import TagBadge from "@/components/gallery/TagBadge"; import StyledLink from "@/components/gallery/StyledLink"; import { cn } from "@/lib/utils"; +import { Link } from "wouter"; export interface ContentInfoPageProps { params: { @@ -62,18 +63,26 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) { const tags = data?.tags ?? []; const classifiedTags = classifyTags(tags); + const contentLocation = `/doc/${params.id}/reader`; + return (
-
- {data.title} -
+ {data.title} +
+ - {data.title} + + + {data.title} + + {classifiedTags.type[0] ?? "N/A"} diff --git a/packages/client/src/page/galleryPage.tsx b/packages/client/src/page/galleryPage.tsx index 81699f5..4ed66bc 100644 --- a/packages/client/src/page/galleryPage.tsx +++ b/packages/client/src/page/galleryPage.tsx @@ -40,6 +40,7 @@ export default function Gallery() { data?.length === 0 &&
No results
} { + // TODO: implement infinite scroll data?.map((x) => { return ( diff --git a/packages/client/src/page/reader/comic.tsx b/packages/client/src/page/reader/comic.tsx deleted file mode 100644 index efccde1..0000000 --- a/packages/client/src/page/reader/comic.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import { Typography, styled } from "@mui/material"; -import React, { RefObject, useEffect, useState } from "react"; -import { useSearchParams } from "react-router-dom"; -import { Document } from "../../accessor/document"; - -type ComicType = "comic" | "artist cg" | "donjinshi" | "western"; - -export type PresentableTag = { - artist: string[]; - group: string[]; - series: string[]; - type: ComicType; - character: string[]; - tags: string[]; -}; - -const ViewMain = styled("div")(({ theme }) => ({ - overflow: "hidden", - width: "100%", - height: "calc(100vh - 64px)", - position: "relative", -})); -const CurrentView = styled("img")(({ theme }) => ({ - maxWidth: "100%", - maxHeight: "100%", - top: "50%", - left: "50%", - transform: "translate(-50%,-50%)", - position: "absolute", -})); - -export const ComicReader = (props: { doc: Document; fullScreenTarget?: RefObject }) => { - const additional = props.doc.additional; - const [searchParams, setSearchParams] = useSearchParams(); - - const curPage = parseInt(searchParams.get("page") ?? "0"); - const setCurPage = (n: number) => { - setSearchParams([["page", n.toString()]]); - }; - if (isNaN(curPage)) { - return Error. Page number is not a number.; - } - if (!("page" in additional)) { - console.error("invalid content : page read fail : " + JSON.stringify(additional)); - return Error. DB error. page restriction; - } - - const maxPage: number = additional["page"] as number; - const PageDown = () => setCurPage(Math.max(curPage - 1, 0)); - const PageUp = () => setCurPage(Math.min(curPage + 1, maxPage - 1)); - - const onKeyUp = (e: KeyboardEvent) => { - console.log(`currently: ${curPage}/${maxPage}`); - if (e.code === "ArrowLeft") { - PageDown(); - } else if (e.code === "ArrowRight") { - PageUp(); - } - }; - - useEffect(() => { - document.addEventListener("keydown", onKeyUp); - return () => { - document.removeEventListener("keydown", onKeyUp); - }; - }, [curPage]); - // theme.mixins.toolbar.minHeight; - return ( - -
- -
-
- ); -}; - -export default ComicReader; diff --git a/packages/client/src/page/reader/comicPage.tsx b/packages/client/src/page/reader/comicPage.tsx new file mode 100644 index 0000000..9be24bc --- /dev/null +++ b/packages/client/src/page/reader/comicPage.tsx @@ -0,0 +1,108 @@ +import { useGalleryDoc } from "@/hook/useGalleryDoc"; +import { cn } from "@/lib/utils"; +import type { Document } from "dbtype/api"; +import { useCallback, useEffect, useRef, useState } from "react"; + +interface ComicPageProps { + params: { + id: string; + }; +} + +function ComicViewer({ + doc, + totalPage, +}: { + doc: Document; + totalPage: number; +}) { + const [curPage, setCurPage] = useState(0); + const [fade, setFade] = useState(false); + const PageDown = useCallback((step: number) => setCurPage(Math.max(curPage - step, 0)), [curPage]); + const PageUp = useCallback((step: number) => setCurPage(Math.min(curPage + step, totalPage - 1)), [curPage, 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){ + const img = new Image(); + img.src = `/api/doc/${doc.id}/comic/${curPage}`; + if (img.complete) { + currentImageRef.current.src = img.src; + 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]); + + return ( +
+
PageDown(1)} /> + main content +
PageUp(1)} /> +
+ ); +} + +export default function ComicPage({ + params +}: ComicPageProps) { + const { data, error, isLoading } = useGalleryDoc(params.id); + + 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 ( + + ) +} \ No newline at end of file diff --git a/packages/client/src/page/reader/reader.tsx b/packages/client/src/page/reader/reader.tsx deleted file mode 100644 index 4f67af6..0000000 --- a/packages/client/src/page/reader/reader.tsx +++ /dev/null @@ -1,80 +0,0 @@ -import { styled, Typography } from "@mui/material"; -import React from "react"; -import { Document, makeThumbnailUrl } from "../../accessor/document"; -import { ComicReader } from "./comic"; -import { VideoReader } from "./video"; - -export interface PagePresenterProp { - doc: Document; - className?: string; - fullScreenTarget?: React.RefObject; -} -interface PagePresenter { - (prop: PagePresenterProp): JSX.Element; -} - -export const getPresenter = (content: Document): PagePresenter => { - switch (content.content_type) { - case "comic": - return ComicReader; - case "video": - return VideoReader; - } - return () => Not implemented reader; -}; -const BackgroundDiv = styled("div")({ - height: "400px", - width: "300px", - backgroundColor: "#272733", - display: "flex", - alignItems: "center", - justifyContent: "center", -}); - -import { useEffect, useRef, useState } from "react"; -import "./thumbnail.css"; - -export function useIsElementInViewport(options?: IntersectionObserverInit) { - const elementRef = useRef(null); - const [isVisible, setIsVisible] = useState(false); - - const callback = (entries: IntersectionObserverEntry[]) => { - const [entry] = entries; - setIsVisible(entry.isIntersecting); - }; - - useEffect(() => { - const observer = new IntersectionObserver(callback, options); - elementRef.current && observer.observe(elementRef.current); - return () => observer.disconnect(); - }, [elementRef, options]); - - return { elementRef, isVisible }; -} - -export function ThumbnailContainer(props: { - content: Document; - className?: string; -}) { - const { elementRef, isVisible } = useIsElementInViewport({}); - const [loaded, setLoaded] = useState(false); - useEffect(() => { - if (isVisible) { - setLoaded(true); - } - }, [isVisible]); - const style = { - maxHeight: "400px", - maxWidth: "min(400px, 100vw)", - }; - const thumbnailurl = makeThumbnailUrl(props.content); - if (props.content.content_type === "video") { - return ; - } else { - return ( - - {loaded && } - - ); - } -} diff --git a/packages/client/src/page/reader/video.tsx b/packages/client/src/page/reader/video.tsx deleted file mode 100644 index d7610aa..0000000 --- a/packages/client/src/page/reader/video.tsx +++ /dev/null @@ -1,14 +0,0 @@ -import React from "react"; -import { Document } from "../../accessor/document"; - -export const VideoReader = (props: { doc: Document }) => { - const id = props.doc.id; - return ( - - ); -}; diff --git a/packages/client/src/page/tags.tsx b/packages/client/src/page/tags.tsx deleted file mode 100644 index 637117e..0000000 --- a/packages/client/src/page/tags.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Box, Paper, Typography } from "@mui/material"; -import { DataGrid, GridColDef } from "@mui/x-data-grid"; -import React, { useEffect, useState } from "react"; -import { LoadingCircle } from "../component/loading"; -import { CommonMenuList, Headline } from "../component/mod"; -import { PagePad } from "../component/pagepad"; - -type TagCount = { - tag_name: string; - occurs: number; -}; - -const tagTableColumn: GridColDef[] = [ - { - field: "tag_name", - headerName: "Tag Name", - width: 200, - }, - { - field: "occurs", - headerName: "Occurs", - width: 100, - type: "number", - }, -]; - -function TagTable() { - const [data, setData] = useState(); - const [error, setErrorMsg] = useState(undefined); - const isLoading = data === undefined; - - useEffect(() => { - loadData(); - }, []); - - if (isLoading) { - return ; - } - if (error !== undefined) { - return {error}; - } - return ( - - - t.tag_name}> - - - ); - - async function loadData() { - try { - const res = await fetch("/api/tags?withCount=true"); - const data = await res.json(); - setData(data); - } catch (e) { - setData([]); - if (e instanceof Error) { - setErrorMsg(e.message); - } else { - console.log(e); - setErrorMsg(""); - } - } - } -} - -export const TagsPage = () => { - const menu = CommonMenuList(); - return ( - - - - - - ); -};