Rework #6
@ -15,14 +15,15 @@ import './App.css'
|
|||||||
// } from "./page/mod";
|
// } from "./page/mod";
|
||||||
|
|
||||||
import { TooltipProvider } from "./components/ui/tooltip.tsx";
|
import { TooltipProvider } from "./components/ui/tooltip.tsx";
|
||||||
|
|
||||||
import Gallery from "./page/galleryPage.tsx";
|
|
||||||
import Layout from "./components/layout/layout.tsx";
|
import Layout from "./components/layout/layout.tsx";
|
||||||
import NotFoundPage from "./page/404.tsx";
|
|
||||||
import LoginPage from "./page/loginPage.tsx";
|
import Gallery from "@/page/galleryPage.tsx";
|
||||||
import ProfilePage from "./page/profilesPage.tsx";
|
import NotFoundPage from "@/page/404.tsx";
|
||||||
import ContentInfoPage from "./page/contentInfoPage.tsx";
|
import LoginPage from "@/page/loginPage.tsx";
|
||||||
import SettingPage from "./page/settingPage.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 = () => {
|
const App = () => {
|
||||||
return (
|
return (
|
||||||
@ -35,10 +36,10 @@ const App = () => {
|
|||||||
<Route path="/profile" component={ProfilePage}/>
|
<Route path="/profile" component={ProfilePage}/>
|
||||||
<Route path="/doc/:id" component={ContentInfoPage}/>
|
<Route path="/doc/:id" component={ContentInfoPage}/>
|
||||||
<Route path="/setting" component={SettingPage} />
|
<Route path="/setting" component={SettingPage} />
|
||||||
|
<Route path="/doc/:id/reader" component={ComicPage}/>
|
||||||
{/*
|
{/*
|
||||||
<Route path="/doc/:id/reader" component={<ReaderPage />}></Route>
|
<Route path="/difference" component={<DifferencePage />}/>
|
||||||
<Route path="/difference" component={<DifferencePage />}></Route>
|
*/}
|
||||||
<Route path="/tags" component={<TagsPage />}></Route>*/}
|
|
||||||
<Route component={NotFoundPage} />
|
<Route component={NotFoundPage} />
|
||||||
</Switch>
|
</Switch>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -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 (
|
|
||||||
<Paper
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
height: props.short ? "400px" : "auto",
|
|
||||||
overflow: "hidden",
|
|
||||||
[theme.breakpoints.down("sm")]: {
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "center",
|
|
||||||
height: "auto",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
elevation={4}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
component={RouterLink}
|
|
||||||
to={{
|
|
||||||
pathname: makeContentReaderUrl(document.id),
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{document.deleted_at === null ? (
|
|
||||||
<ThumbnailContainer content={document} />
|
|
||||||
) : (
|
|
||||||
<Typography variant="h4">Deleted</Typography>
|
|
||||||
)}
|
|
||||||
</Link>
|
|
||||||
<Box>
|
|
||||||
<Link variant="h5" color="inherit" component={RouterLink} to={{ pathname: url }}>
|
|
||||||
{document.title}
|
|
||||||
</Link>
|
|
||||||
<Box>
|
|
||||||
{props.short ? (
|
|
||||||
<Box>
|
|
||||||
{document.tags.map((x) => (
|
|
||||||
<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
) : (
|
|
||||||
<ComicDetailTag
|
|
||||||
tags={document.tags}
|
|
||||||
path={document.basepath + "/" + document.filename}
|
|
||||||
createdAt={document.created_at}
|
|
||||||
deletedAt={document.deleted_at != null ? document.deleted_at : undefined}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
{document.deleted_at != null && (
|
|
||||||
<Button
|
|
||||||
onClick={() => {
|
|
||||||
documentDelete(document.id);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Delete
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</Paper>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
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 (
|
|
||||||
<Grid container>
|
|
||||||
{tagKind.map((key) => (
|
|
||||||
<React.Fragment key={key}>
|
|
||||||
<Grid item xs={3}>
|
|
||||||
<Typography variant="subtitle1">{key}</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={9}>
|
|
||||||
<Box>
|
|
||||||
{tagTable[key].length !== 0
|
|
||||||
? tagTable[key].map((elem, i) => {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Link to={`/search?allow_tag=${key}:${encodeURIComponent(elem)}`} component={RouterLink}>
|
|
||||||
{elem}
|
|
||||||
</Link>
|
|
||||||
{i < tagTable[key].length - 1 ? "," : ""}
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
})
|
|
||||||
: "N/A"}
|
|
||||||
</Box>
|
|
||||||
</Grid>
|
|
||||||
</React.Fragment>
|
|
||||||
))}
|
|
||||||
{prop.path != undefined && (
|
|
||||||
<>
|
|
||||||
<Grid item xs={3}>
|
|
||||||
<Typography variant="subtitle1">Path</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={9}>
|
|
||||||
<Box>{prop.path}</Box>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{prop.createdAt != undefined && (
|
|
||||||
<>
|
|
||||||
<Grid item xs={3}>
|
|
||||||
<Typography variant="subtitle1">CreatedAt</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={9}>
|
|
||||||
<Box>{new Date(prop.createdAt).toUTCString()}</Box>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
{prop.deletedAt != undefined && (
|
|
||||||
<>
|
|
||||||
<Grid item xs={3}>
|
|
||||||
<Typography variant="subtitle1">DeletedAt</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={9}>
|
|
||||||
<Box>{new Date(prop.deletedAt).toUTCString()}</Box>
|
|
||||||
</Grid>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Grid item xs={3}>
|
|
||||||
<Typography variant="subtitle1">Tags</Typography>
|
|
||||||
</Grid>
|
|
||||||
<Grid item xs={9}>
|
|
||||||
{allTag.map((x) => (
|
|
||||||
<TagChip key={x} label={x} clickable tagname={x} size="small"></TagChip>
|
|
||||||
))}
|
|
||||||
</Grid>
|
|
||||||
</Grid>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,5 +0,0 @@
|
|||||||
export * from "./contentinfo";
|
|
||||||
export * from "./headline";
|
|
||||||
export * from "./loading";
|
|
||||||
export * from "./navlist";
|
|
||||||
export * from "./tagchip";
|
|
@ -1,8 +1,7 @@
|
|||||||
import type { Document } from "dbtype/api";
|
import type { Document } from "dbtype/api";
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx";
|
||||||
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
import TagBadge from "@/components/gallery/TagBadge.tsx";
|
||||||
import { Fragment, ReactNode, useEffect, useRef, useState } from "react";
|
import { Fragment, useEffect, useRef, useState } from "react";
|
||||||
import { Link, useLocation } from "wouter";
|
|
||||||
import { LazyImage } from "./LazyImage.tsx";
|
import { LazyImage } from "./LazyImage.tsx";
|
||||||
import StyledLink from "./StyledLink.tsx";
|
import StyledLink from "./StyledLink.tsx";
|
||||||
|
|
||||||
|
@ -3,6 +3,7 @@ import { useGalleryDoc } from "../hook/useGalleryDoc";
|
|||||||
import TagBadge from "@/components/gallery/TagBadge";
|
import TagBadge from "@/components/gallery/TagBadge";
|
||||||
import StyledLink from "@/components/gallery/StyledLink";
|
import StyledLink from "@/components/gallery/StyledLink";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
import { Link } from "wouter";
|
||||||
|
|
||||||
export interface ContentInfoPageProps {
|
export interface ContentInfoPageProps {
|
||||||
params: {
|
params: {
|
||||||
@ -62,18 +63,26 @@ export function ContentInfoPage({ params }: ContentInfoPageProps) {
|
|||||||
const tags = data?.tags ?? [];
|
const tags = data?.tags ?? [];
|
||||||
const classifiedTags = classifyTags(tags);
|
const classifiedTags = classifyTags(tags);
|
||||||
|
|
||||||
|
const contentLocation = `/doc/${params.id}/reader`;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-4">
|
<div className="p-4">
|
||||||
<div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
|
<Link to={contentLocation}>
|
||||||
|
<div className="m-auto h-[400px] mb-4 flex justify-center items-center flex-none bg-[#272733]
|
||||||
rounded-xl shadow-lg overflow-hidden">
|
rounded-xl shadow-lg overflow-hidden">
|
||||||
<img
|
<img
|
||||||
className="max-w-full max-h-full object-cover object-center"
|
className="max-w-full max-h-full object-cover object-center"
|
||||||
src={`/api/doc/${data.id}/comic/thumbnail`}
|
src={`/api/doc/${data.id}/comic/thumbnail`}
|
||||||
alt={data.title} />
|
alt={data.title} />
|
||||||
</div>
|
</div>
|
||||||
|
</Link>
|
||||||
<Card className="flex-1">
|
<Card className="flex-1">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>{data.title}</CardTitle>
|
<CardTitle>
|
||||||
|
<StyledLink to={contentLocation}>
|
||||||
|
{data.title}
|
||||||
|
</StyledLink>
|
||||||
|
</CardTitle>
|
||||||
<CardDescription>
|
<CardDescription>
|
||||||
<StyledLink to={`/search?allow_tag=type:${classifiedTags.type[0] ?? ""}`} className="text-sm">
|
<StyledLink to={`/search?allow_tag=type:${classifiedTags.type[0] ?? ""}`} className="text-sm">
|
||||||
{classifiedTags.type[0] ?? "N/A"}
|
{classifiedTags.type[0] ?? "N/A"}
|
||||||
|
@ -40,6 +40,7 @@ export default function Gallery() {
|
|||||||
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
|
data?.length === 0 && <div className="p-4 text-3xl">No results</div>
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
|
// TODO: implement infinite scroll
|
||||||
data?.map((x) => {
|
data?.map((x) => {
|
||||||
return (
|
return (
|
||||||
<GalleryCard doc={x} key={x.id} />
|
<GalleryCard doc={x} key={x.id} />
|
||||||
|
@ -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<HTMLDivElement> }) => {
|
|
||||||
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 <Typography>Error. Page number is not a number.</Typography>;
|
|
||||||
}
|
|
||||||
if (!("page" in additional)) {
|
|
||||||
console.error("invalid content : page read fail : " + JSON.stringify(additional));
|
|
||||||
return <Typography>Error. DB error. page restriction</Typography>;
|
|
||||||
}
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<ViewMain ref={props.fullScreenTarget}>
|
|
||||||
<div
|
|
||||||
onClick={PageDown}
|
|
||||||
style={{ left: "0", width: "50%", position: "absolute", height: "100%", zIndex: 100 }}
|
|
||||||
></div>
|
|
||||||
<CurrentView onClick={PageUp} src={`/api/doc/${props.doc.id}/comic/${curPage}`}></CurrentView>
|
|
||||||
<div
|
|
||||||
onClick={PageUp}
|
|
||||||
style={{ right: "0", width: "50%", position: "absolute", height: "100%", zIndex: 100 }}
|
|
||||||
></div>
|
|
||||||
</ViewMain>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ComicReader;
|
|
108
packages/client/src/page/reader/comicPage.tsx
Normal file
108
packages/client/src/page/reader/comicPage.tsx
Normal file
@ -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<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){
|
||||||
|
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: <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]);
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function ComicPage({
|
||||||
|
params
|
||||||
|
}: ComicPageProps) {
|
||||||
|
const { data, error, isLoading } = useGalleryDoc(params.id);
|
||||||
|
|
||||||
|
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 (
|
||||||
|
<ComicViewer doc={data} totalPage={data.additional.page as number} />
|
||||||
|
)
|
||||||
|
}
|
@ -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<HTMLDivElement>;
|
|
||||||
}
|
|
||||||
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 () => <Typography variant="h2">Not implemented reader</Typography>;
|
|
||||||
};
|
|
||||||
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<T extends HTMLElement>(options?: IntersectionObserverInit) {
|
|
||||||
const elementRef = useRef<T>(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<HTMLDivElement>({});
|
|
||||||
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 <video src={thumbnailurl} muted autoPlay loop className={props.className} style={style}></video>;
|
|
||||||
} else {
|
|
||||||
return (
|
|
||||||
<BackgroundDiv ref={elementRef}>
|
|
||||||
{loaded && <img src={thumbnailurl} className={props.className + " thumbnail_img"} loading="lazy"></img>}
|
|
||||||
</BackgroundDiv>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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 (
|
|
||||||
<video
|
|
||||||
controls
|
|
||||||
autoPlay
|
|
||||||
src={`/api/doc/${props.doc.id}/video`}
|
|
||||||
style={{ maxHeight: "100%", maxWidth: "100%" }}
|
|
||||||
></video>
|
|
||||||
);
|
|
||||||
};
|
|
@ -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<TagCount[] | undefined>();
|
|
||||||
const [error, setErrorMsg] = useState<string | undefined>(undefined);
|
|
||||||
const isLoading = data === undefined;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
loadData();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <LoadingCircle />;
|
|
||||||
}
|
|
||||||
if (error !== undefined) {
|
|
||||||
return <Typography variant="h3">{error}</Typography>;
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<Box sx={{ height: "400px", width: "100%" }}>
|
|
||||||
<Paper sx={{ height: "100%" }} elevation={2}>
|
|
||||||
<DataGrid rows={data} columns={tagTableColumn} getRowId={(t) => t.tag_name}></DataGrid>
|
|
||||||
</Paper>
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
|
|
||||||
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 (
|
|
||||||
<Headline menu={menu}>
|
|
||||||
<PagePad>
|
|
||||||
<TagTable></TagTable>
|
|
||||||
</PagePad>
|
|
||||||
</Headline>
|
|
||||||
);
|
|
||||||
};
|
|
Loading…
Reference in New Issue
Block a user