reader page
This commit is contained in:
parent
2df64ac2fe
commit
ef77706c56
@ -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 = () => {
|
||||
<Route path="/profile" component={ProfilePage}/>
|
||||
<Route path="/doc/:id" component={ContentInfoPage}/>
|
||||
<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>
|
||||
<Route path="/tags" component={<TagsPage />}></Route>*/}
|
||||
<Route path="/difference" component={<DifferencePage />}/>
|
||||
*/}
|
||||
<Route component={NotFoundPage} />
|
||||
</Switch>
|
||||
</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 { 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";
|
||||
|
||||
|
@ -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 (
|
||||
<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">
|
||||
<img
|
||||
className="max-w-full max-h-full object-cover object-center"
|
||||
src={`/api/doc/${data.id}/comic/thumbnail`}
|
||||
alt={data.title} />
|
||||
</div>
|
||||
<img
|
||||
className="max-w-full max-h-full object-cover object-center"
|
||||
src={`/api/doc/${data.id}/comic/thumbnail`}
|
||||
alt={data.title} />
|
||||
</div>
|
||||
</Link>
|
||||
<Card className="flex-1">
|
||||
<CardHeader>
|
||||
<CardTitle>{data.title}</CardTitle>
|
||||
<CardTitle>
|
||||
<StyledLink to={contentLocation}>
|
||||
{data.title}
|
||||
</StyledLink>
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
<StyledLink to={`/search?allow_tag=type:${classifiedTags.type[0] ?? ""}`} className="text-sm">
|
||||
{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>
|
||||
}
|
||||
{
|
||||
// TODO: implement infinite scroll
|
||||
data?.map((x) => {
|
||||
return (
|
||||
<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