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}
+
+
{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)} />
+
+
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 (
-
-
-
-
-
- );
-};