mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-05-31 01:23:02 +02:00
Compare commits
4 Commits
bbad467fe6
...
11549a07c3
Author | SHA1 | Date | |
---|---|---|---|
11549a07c3 | |||
79c13ceb1d | |||
0907031583 | |||
00932bf6bd |
@ -38,7 +38,7 @@ export default function App () {
|
|||||||
<Badge invisible sx={{margin: "8px !important"}}>
|
<Badge invisible sx={{margin: "8px !important"}}>
|
||||||
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
|
<Card onClick={() => setShowSearch(true)} sx={{height:"fit-content",width:"fit-content"}}>
|
||||||
<CardCover sx={{margin:"var(--Card-padding)"}}>
|
<CardCover sx={{margin:"var(--Card-padding)"}}>
|
||||||
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + "px"}} />
|
<img src={"/blahaj.png"} style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} />
|
||||||
</CardCover>
|
</CardCover>
|
||||||
<CardCover sx={{
|
<CardCover sx={{
|
||||||
background: 'rgba(234, 119, 246, 0.14)',
|
background: 'rgba(234, 119, 246, 0.14)',
|
||||||
@ -46,7 +46,7 @@ export default function App () {
|
|||||||
webkitBackdropFilter: 'blur(6.9px)',
|
webkitBackdropFilter: 'blur(6.9px)',
|
||||||
}}/>
|
}}/>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Box style={{height: CardHeight + "px", width: CardWidth + "px"}} >
|
<Box style={{height: CardHeight + "px", width: CardWidth + 10 + "px"}} >
|
||||||
<Typography level={"h1"}>Search</Typography>
|
<Typography level={"h1"}>Search</Typography>
|
||||||
</Box>
|
</Box>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
@ -1,23 +1,10 @@
|
|||||||
import {Chip, ColorPaletteProp, Skeleton} from "@mui/joy";
|
import {Chip, ColorPaletteProp} from "@mui/joy";
|
||||||
import {useContext, useEffect, useState} from "react";
|
|
||||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
|
||||||
import IAuthor from "../api/types/IAuthor.ts";
|
import IAuthor from "../api/types/IAuthor.ts";
|
||||||
import {GetAuthor} from "../api/Query.tsx";
|
|
||||||
|
|
||||||
export default function AuthorTag({authorId, color} : { authorId: string | undefined, color?: ColorPaletteProp }) {
|
|
||||||
const useAuthor = authorId ?? "AuthorId";
|
|
||||||
const apiUri = useContext(ApiUriContext);
|
|
||||||
|
|
||||||
const [author, setAuthor] = useState<IAuthor>();
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
GetAuthor(apiUri, useAuthor).then(setAuthor).finally(() => setLoading(false));
|
|
||||||
}, [authorId]);
|
|
||||||
|
|
||||||
|
export default function AuthorTag({author, color} : {author: IAuthor, color?: ColorPaletteProp }) {
|
||||||
return (
|
return (
|
||||||
<Chip variant={"outlined"} size={"md"} color={color??"primary"}>
|
<Chip variant={"outlined"} size={"md"} color={color??"primary"}>
|
||||||
<Skeleton variant={"text"} loading={loading}>{author?.authorName ?? "Load Failed"}</Skeleton>
|
{author.authorName ?? "Load Failed"}
|
||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -1,25 +1,10 @@
|
|||||||
import {Chip, Skeleton, Link, ColorPaletteProp} from "@mui/joy";
|
import {Chip, Link, ColorPaletteProp} from "@mui/joy";
|
||||||
import {useContext, useEffect, useState} from "react";
|
|
||||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
|
||||||
import {GetLink} from "../api/Query.tsx";
|
|
||||||
import ILink from "../api/types/ILink.ts";
|
import ILink from "../api/types/ILink.ts";
|
||||||
|
|
||||||
export default function LinkTag({linkId, color} : { linkId: string | undefined, color?: ColorPaletteProp }) {
|
export default function LinkTag({link, color} : { link: ILink | undefined, color?: ColorPaletteProp }) {
|
||||||
const useLink = linkId ?? "LinkId";
|
|
||||||
const apiUri = useContext(ApiUriContext);
|
|
||||||
|
|
||||||
const [link, setLink] = useState<ILink>();
|
|
||||||
const [loading, setLoading] = useState<boolean>(true);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
GetLink(apiUri, useLink).then(setLink).finally(() => setLoading(false));
|
|
||||||
}, [linkId]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Chip variant={"soft"} size={"sm"} color={color??"primary"}>
|
<Chip variant={"soft"} size={"sm"} color={color??"primary"}>
|
||||||
<Skeleton variant={"text"} loading={loading}>
|
<Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
|
||||||
<Link sx={{textDecoration:"underline"}} level={"body-xs"} href={link?.linkUrl}>{link?.linkProvider??"Load Failed"}</Link>
|
|
||||||
</Skeleton>
|
|
||||||
</Chip>
|
</Chip>
|
||||||
);
|
);
|
||||||
}
|
}
|
@ -2,25 +2,16 @@ import {
|
|||||||
Badge,
|
Badge,
|
||||||
Box,
|
Box,
|
||||||
Card,
|
Card,
|
||||||
CardActions,
|
|
||||||
CardContent, CardCover,
|
CardContent, CardCover,
|
||||||
Chip, CircularProgress,
|
|
||||||
Input,
|
|
||||||
Link,
|
Link,
|
||||||
Skeleton,
|
|
||||||
Stack,
|
|
||||||
Typography
|
|
||||||
} from "@mui/joy";
|
} from "@mui/joy";
|
||||||
import IManga, {DefaultManga} from "../api/types/IManga.ts";
|
import IManga, {DefaultManga} from "../api/types/IManga.ts";
|
||||||
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
import {CSSProperties, ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
import {GetLatestChapterAvailable, GetMangaById, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
|
import {GetMangaById, GetMangaCoverImageUrl} from "../api/Manga.tsx";
|
||||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
||||||
import AuthorTag from "./AuthorTag.tsx";
|
|
||||||
import LinkTag from "./LinkTag.tsx";
|
|
||||||
import {ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
import {ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
||||||
import IChapter from "../api/types/IChapter.ts";
|
|
||||||
import MarkdownPreview from "@uiw/react-markdown-preview";
|
|
||||||
import {SxProps} from "@mui/joy/styles/types";
|
import {SxProps} from "@mui/joy/styles/types";
|
||||||
|
import MangaPopup from "./MangaPopup.tsx";
|
||||||
|
|
||||||
export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
|
export function MangaFromId({mangaId, children} : { mangaId: string, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined }){
|
||||||
const [manga, setManga] = useState(DefaultManga);
|
const [manga, setManga] = useState(DefaultManga);
|
||||||
@ -37,49 +28,39 @@ export function MangaFromId({mangaId, children} : { mangaId: string, children?:
|
|||||||
loadManga();
|
loadManga();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
return <Manga manga={manga} loading={loading} children={children} />
|
return (
|
||||||
|
<>
|
||||||
|
{loading ? <></> : <Manga manga={manga} children={children} /> }
|
||||||
|
</>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CardWidth = 190;
|
export const CardWidth = 190;
|
||||||
export const CardHeight = 300;
|
export const CardHeight = 300;
|
||||||
|
|
||||||
export function Manga({manga, children, loading} : { manga: IManga | undefined, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined, loading?: boolean}) {
|
export function Manga({manga: manga, children} : { manga: IManga, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
|
||||||
const useManga = manga ?? DefaultManga;
|
|
||||||
loading = loading ?? false;
|
|
||||||
const CoverRef = useRef<HTMLImageElement>(null);
|
const CoverRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
const apiUri = useContext(ApiUriContext);
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
const [expanded, setExpanded] = useState(false);
|
const [expanded, setExpanded] = useState(false);
|
||||||
|
|
||||||
const [mangaMaxChapter, setMangaMaxChapter] = useState<IChapter>();
|
|
||||||
const [maxChapterLoading, setMaxChapterLoading] = useState<boolean>(true);
|
|
||||||
const LoadMaxChapter = useCallback(() => {
|
|
||||||
setMaxChapterLoading(true);
|
|
||||||
GetLatestChapterAvailable(apiUri, useManga.mangaId)
|
|
||||||
.then(setMangaMaxChapter)
|
|
||||||
.finally(() => setMaxChapterLoading(false));
|
|
||||||
}, [useManga, apiUri]);
|
|
||||||
|
|
||||||
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
|
|
||||||
const updateIgnoreThreshhold = useCallback((value: number) => {
|
|
||||||
setUpdatingThreshold(true);
|
|
||||||
SetIgnoreThreshold(apiUri, useManga.mangaId, value).finally(() => setUpdatingThreshold(false));
|
|
||||||
},[useManga, apiUri])
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
LoadMaxChapter();
|
|
||||||
LoadMangaCover();
|
LoadMangaCover();
|
||||||
}, [useManga]);
|
}, [manga]);
|
||||||
|
|
||||||
const LoadMangaCover = useCallback(() => {
|
const LoadMangaCover = useCallback(() => {
|
||||||
if(CoverRef.current == null)
|
if(CoverRef.current == null)
|
||||||
return;
|
return;
|
||||||
const coverUrl = GetMangaCoverImageUrl(apiUri, useManga.mangaId, CoverRef.current);
|
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
|
||||||
if(CoverRef.current.src == coverUrl)
|
if(CoverRef.current.src == coverUrl)
|
||||||
return;
|
return;
|
||||||
CoverRef.current.src = GetMangaCoverImageUrl(apiUri, useManga.mangaId, CoverRef.current);
|
|
||||||
}, [useManga, apiUri])
|
//Check if we can fetch the image exists (by fetching it), only then update
|
||||||
|
getData(coverUrl).then(() => {
|
||||||
|
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
||||||
|
});
|
||||||
|
}, [manga, apiUri])
|
||||||
|
|
||||||
const coverSx : SxProps = {
|
const coverSx : SxProps = {
|
||||||
height: CardHeight + "px",
|
height: CardHeight + "px",
|
||||||
@ -87,12 +68,6 @@ export function Manga({manga, children, loading} : { manga: IManga | undefined,
|
|||||||
position: "relative",
|
position: "relative",
|
||||||
}
|
}
|
||||||
|
|
||||||
const descriptionSx : SxProps = {
|
|
||||||
height: CardHeight + "px",
|
|
||||||
width: CardWidth * 2 + "px",
|
|
||||||
position: "relative"
|
|
||||||
}
|
|
||||||
|
|
||||||
const coverCss : CSSProperties = {
|
const coverCss : CSSProperties = {
|
||||||
maxHeight: "calc("+CardHeight+"px + 2rem)",
|
maxHeight: "calc("+CardHeight+"px + 2rem)",
|
||||||
maxWidth: "calc("+CardWidth+"px + 2rem)",
|
maxWidth: "calc("+CardWidth+"px + 2rem)",
|
||||||
@ -100,20 +75,19 @@ export function Manga({manga, children, loading} : { manga: IManga | undefined,
|
|||||||
|
|
||||||
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
|
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
|
||||||
|
|
||||||
const mangaName = useManga.name.length > 30 ? useManga.name.substring(0, 27) + "..." : useManga.name;
|
const mangaName = manga.name.length > 30 ? manga.name.substring(0, 27) + "..." : manga.name;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Badge sx={{margin:"8px !important"}} badgeContent={useManga.mangaConnectorId} color={ReleaseStatusToPalette(useManga.releaseStatus)} size={"lg"}>
|
<Badge sx={{margin:"8px !important"}} badgeContent={manga.mangaConnectorName} color={ReleaseStatusToPalette(manga.releaseStatus)} size={"lg"}>
|
||||||
<Card sx={{height:"fit-content",width:"fit-content"}} onClick={(e) => {
|
<Card sx={{height:"fit-content",width:"fit-content"}} onClick={(e) => {
|
||||||
const target = e.target as HTMLElement;
|
const target = e.target as HTMLElement;
|
||||||
if(interactiveElements.find(x => x == target.localName) == undefined)
|
if(interactiveElements.find(x => x == target.localName) == undefined)
|
||||||
setExpanded(!expanded)}
|
setExpanded(!expanded)}
|
||||||
}>
|
}>
|
||||||
<CardCover>
|
<CardCover>
|
||||||
<img style={coverCss} src="/blahaj.png" alt="Manga Cover"
|
<img style={coverCss} src={GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current)} alt="Manga Cover"
|
||||||
ref={CoverRef}
|
ref={CoverRef}
|
||||||
onLoad={LoadMangaCover}
|
onLoad={LoadMangaCover}/>
|
||||||
onResize={LoadMangaCover}/>
|
|
||||||
</CardCover>
|
</CardCover>
|
||||||
<CardCover sx={{
|
<CardCover sx={{
|
||||||
background:
|
background:
|
||||||
@ -121,61 +95,12 @@ export function Manga({manga, children, loading} : { manga: IManga | undefined,
|
|||||||
}}/>
|
}}/>
|
||||||
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
|
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
|
||||||
<Box sx={coverSx}>
|
<Box sx={coverSx}>
|
||||||
<Skeleton loading={loading}>
|
<Link href={manga.websiteUrl} level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
|
||||||
<Link href={useManga.websiteUrl} level={"h3"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
|
{mangaName}
|
||||||
{mangaName}
|
</Link>
|
||||||
</Link>
|
|
||||||
</Skeleton>
|
|
||||||
</Box>
|
</Box>
|
||||||
{
|
|
||||||
expanded ?
|
|
||||||
<Box sx={descriptionSx}>
|
|
||||||
<Skeleton loading={loading} variant={"text"} level={"title-lg"}>
|
|
||||||
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
|
|
||||||
{useManga.authorIds.map(authorId => <AuthorTag key={authorId} authorId={authorId} color={"success"} />)}
|
|
||||||
{useManga.tags.map(tag => <Chip key={tag} variant={"soft"} size={"md"} color={"primary"}>{tag}</Chip>)}
|
|
||||||
{useManga.linkIds.map(linkId => <LinkTag key={linkId} linkId={linkId} color={"warning"} />)}
|
|
||||||
</Stack>
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton loading={loading} sx={{maxHeight:"300px"}}>
|
|
||||||
<MarkdownPreview source={useManga.description} style={{backgroundColor: "transparent", color: "black", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
|
|
||||||
</Skeleton>
|
|
||||||
</Box>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</CardContent>
|
</CardContent>
|
||||||
{
|
<MangaPopup manga={manga} open={expanded}>{children}</MangaPopup>
|
||||||
expanded ?
|
|
||||||
<CardActions sx={{justifyContent:"space-between"}}>
|
|
||||||
<Skeleton loading={loading} sx={{maxHeight: "30px", maxWidth:"calc(100% - 40px)"}}>
|
|
||||||
<Input
|
|
||||||
type={"number"}
|
|
||||||
placeholder={"0.0"}
|
|
||||||
startDecorator={
|
|
||||||
<>
|
|
||||||
{
|
|
||||||
updatingThreshold ?
|
|
||||||
<CircularProgress color={"primary"} size={"sm"} />
|
|
||||||
: <Typography>Ch.</Typography>
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
endDecorator={
|
|
||||||
<Typography>
|
|
||||||
<Skeleton loading={maxChapterLoading}>
|
|
||||||
/{mangaMaxChapter?.chapterNumber??"Load Failed"}
|
|
||||||
</Skeleton>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
sx={{width:"min-content"}}
|
|
||||||
size={"md"}
|
|
||||||
onChange={(e) => updateIgnoreThreshhold(e.currentTarget.valueAsNumber)}
|
|
||||||
/>
|
|
||||||
{children}
|
|
||||||
</Skeleton>
|
|
||||||
</CardActions>
|
|
||||||
: null
|
|
||||||
}
|
|
||||||
</Card>
|
</Card>
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
|
110
tranga-website/src/Components/MangaPopup.tsx
Normal file
110
tranga-website/src/Components/MangaPopup.tsx
Normal file
@ -0,0 +1,110 @@
|
|||||||
|
import IManga from "../api/types/IManga.ts";
|
||||||
|
import {Badge, Box, Chip, CircularProgress, Drawer, Input, Skeleton, Stack, Typography} from "@mui/joy";
|
||||||
|
import {ReactElement, useCallback, useContext, useEffect, useRef, useState} from "react";
|
||||||
|
import {GetLatestChapterAvailable, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
|
||||||
|
import {ApiUriContext, getData} from "../api/fetchApi.tsx";
|
||||||
|
import AuthorTag from "./AuthorTag.tsx";
|
||||||
|
import LinkTag from "./LinkTag.tsx";
|
||||||
|
import MarkdownPreview from "@uiw/react-markdown-preview";
|
||||||
|
import {CardHeight} from "./Manga.tsx";
|
||||||
|
import IChapter from "../api/types/IChapter.ts";
|
||||||
|
import {MangaReleaseStatus, ReleaseStatusToPalette} from "../api/types/EnumMangaReleaseStatus.ts";
|
||||||
|
|
||||||
|
|
||||||
|
export default function MangaPopup({manga, open, children} : {manga: IManga | null, open: boolean, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined}) {
|
||||||
|
|
||||||
|
const apiUri = useContext(ApiUriContext);
|
||||||
|
|
||||||
|
const CoverRef = useRef<HTMLImageElement>(null);
|
||||||
|
|
||||||
|
const LoadMangaCover = useCallback(() => {
|
||||||
|
if(CoverRef.current == null || manga == null)
|
||||||
|
return;
|
||||||
|
const coverUrl = GetMangaCoverImageUrl(apiUri, manga.mangaId, CoverRef.current);
|
||||||
|
if(CoverRef.current.src == coverUrl)
|
||||||
|
return;
|
||||||
|
|
||||||
|
//Check if we can fetch the image exists (by fetching it), only then update
|
||||||
|
getData(coverUrl).then(() => {
|
||||||
|
if(CoverRef.current) CoverRef.current.src = coverUrl;
|
||||||
|
});
|
||||||
|
}, [manga, apiUri])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if(!open)
|
||||||
|
return;
|
||||||
|
LoadMaxChapter();
|
||||||
|
LoadMangaCover();
|
||||||
|
}, [open]);
|
||||||
|
|
||||||
|
const [mangaMaxChapter, setMangaMaxChapter] = useState<IChapter>();
|
||||||
|
const [maxChapterLoading, setMaxChapterLoading] = useState<boolean>(true);
|
||||||
|
const LoadMaxChapter = useCallback(() => {
|
||||||
|
if(manga == null)
|
||||||
|
return;
|
||||||
|
setMaxChapterLoading(true);
|
||||||
|
GetLatestChapterAvailable(apiUri, manga.mangaId)
|
||||||
|
.then(setMangaMaxChapter)
|
||||||
|
.finally(() => setMaxChapterLoading(false));
|
||||||
|
}, [manga, apiUri]);
|
||||||
|
|
||||||
|
const [updatingThreshold, setUpdatingThreshold] = useState<boolean>(false);
|
||||||
|
const updateIgnoreThreshhold = useCallback((value: number) => {
|
||||||
|
if(manga == null)
|
||||||
|
return;
|
||||||
|
setUpdatingThreshold(true);
|
||||||
|
SetIgnoreThreshold(apiUri, manga.mangaId, value).finally(() => setUpdatingThreshold(false));
|
||||||
|
},[manga, apiUri])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Drawer anchor="bottom" size="lg" open={open}>
|
||||||
|
<Stack direction="column" spacing={2} margin={"10px"}>
|
||||||
|
{ /* Cover and Description */ }
|
||||||
|
<Stack direction="row" spacing={2} margin={"10px"}>
|
||||||
|
<Badge sx={{margin:"8px !important"}} badgeContent={manga?.mangaConnectorName} color={ReleaseStatusToPalette(manga?.releaseStatus??MangaReleaseStatus.Unreleased)} size={"lg"}>
|
||||||
|
<img src="/blahaj.png" alt="Manga Cover"
|
||||||
|
ref={CoverRef}
|
||||||
|
onLoad={LoadMangaCover}/>
|
||||||
|
</Badge>
|
||||||
|
<Box>
|
||||||
|
<Typography level={"h2"} marginTop={"20px"}>{manga?.name}</Typography>
|
||||||
|
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5} sx={{maxHeight:CardHeight*0.3+"px", overflowY:"auto", scrollbarWidth: "thin"}}>
|
||||||
|
{manga?.authors?.map(author => <AuthorTag key={author.authorId} author={author} color={"success"} />)}
|
||||||
|
{manga?.mangaTags?.map(tag => <Chip key={tag.tag} variant={"soft"} size={"md"} color={"primary"}>{tag.tag}</Chip>)}
|
||||||
|
{manga?.links?.map(link => <LinkTag key={link.linkId} link={link} color={"warning"} />)}
|
||||||
|
</Stack>
|
||||||
|
<MarkdownPreview source={manga?.description} style={{backgroundColor: "transparent", color: "var(--joy-palette-neutral-50)", maxHeight:CardHeight*0.7+"px", overflowY:"auto", marginTop:"10px", scrollbarWidth: "thin"}} />
|
||||||
|
</Box>
|
||||||
|
</Stack>
|
||||||
|
|
||||||
|
{ /* Actions */ }
|
||||||
|
<Stack direction="row" spacing={2}>
|
||||||
|
<Input
|
||||||
|
type={"number"}
|
||||||
|
placeholder={"0.0"}
|
||||||
|
startDecorator={
|
||||||
|
<>
|
||||||
|
{
|
||||||
|
updatingThreshold ?
|
||||||
|
<CircularProgress color={"primary"} size={"sm"} />
|
||||||
|
: <Typography>Ch.</Typography>
|
||||||
|
}
|
||||||
|
</>
|
||||||
|
}
|
||||||
|
endDecorator={
|
||||||
|
<Typography>
|
||||||
|
<Skeleton loading={maxChapterLoading}>
|
||||||
|
/{mangaMaxChapter?.chapterNumber??"-"}
|
||||||
|
</Skeleton>
|
||||||
|
</Typography>
|
||||||
|
}
|
||||||
|
sx={{width:"min-content"}}
|
||||||
|
size={"md"}
|
||||||
|
onChange={(e) => updateIgnoreThreshhold(e.currentTarget.valueAsNumber)}
|
||||||
|
/>
|
||||||
|
{children}
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Drawer>
|
||||||
|
);
|
||||||
|
}
|
@ -164,7 +164,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
|||||||
})}
|
})}
|
||||||
</Select>
|
</Select>
|
||||||
<Button disabled={localLibrariesLoading || selectedLibraryId === undefined} onClick={() => {
|
<Button disabled={localLibrariesLoading || selectedLibraryId === undefined} onClick={() => {
|
||||||
CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: selectedLibraryId!,recurrenceTimeMs: 1000 * 60 * 60 * 3})
|
CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: selectedLibraryId!,recurrenceTimeMs: 1000 * 60 * 60 * 3, language: "en"})
|
||||||
}} endDecorator={<Add />}>Watch</Button>
|
}} endDecorator={<Add />}>Watch</Button>
|
||||||
</Manga>)}
|
</Manga>)}
|
||||||
</Stack>
|
</Stack>
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import {deleteData, getData, patchData, postData, putData} from "./fetchApi";
|
import {deleteData, getData, patchData, postData, putData} from "./fetchApi";
|
||||||
import IJob, {JobState, JobType} from "./types/Jobs/IJob";
|
import IJob, {JobState, JobType} from "./types/Jobs/IJob";
|
||||||
import IModifyJobRecord from "./types/records/IModifyJobRecord";
|
import IModifyJobRecord from "./types/records/IModifyJobRecord";
|
||||||
import IDownloadAvailableJobsRecord from "./types/records/IDownloadAvailableJobsRecord.ts";
|
import IDownloadAvailableChaptersJobRecord from "./types/records/IDownloadAvailableChaptersJobRecord.ts";
|
||||||
|
|
||||||
export const GetAllJobs = async (apiUri: string) : Promise<IJob[]> => {
|
export const GetAllJobs = async (apiUri: string) : Promise<IJob[]> => {
|
||||||
return await getData(`${apiUri}/v2/Job`) as Promise<IJob[]>;
|
return await getData(`${apiUri}/v2/Job`) as Promise<IJob[]>;
|
||||||
@ -54,7 +54,7 @@ export const ModifyJob = async (apiUri: string, jobId: string, modifyData: IModi
|
|||||||
return await patchData(`${apiUri}/v2/Job/${jobId}`, modifyData) as Promise<IJob>;
|
return await patchData(`${apiUri}/v2/Job/${jobId}`, modifyData) as Promise<IJob>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateDownloadAvailableChaptersJob = async (apiUri: string, mangaId: string, data: IDownloadAvailableJobsRecord) : Promise<string[]> => {
|
export const CreateDownloadAvailableChaptersJob = async (apiUri: string, mangaId: string, data: IDownloadAvailableChaptersJobRecord) : Promise<string[]> => {
|
||||||
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
if(mangaId === undefined || mangaId === null || mangaId.length < 1)
|
||||||
return Promise.reject("mangaId was not provided");
|
return Promise.reject("mangaId was not provided");
|
||||||
if(data === undefined || data === null)
|
if(data === undefined || data === null)
|
||||||
|
@ -24,7 +24,7 @@ export const DeleteManga = async (apiUri: string, mangaId: string) : Promise<voi
|
|||||||
return await deleteData(`${apiUri}/v2/Manga/${mangaId}`);
|
return await deleteData(`${apiUri}/v2/Manga/${mangaId}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined) : string => {
|
export const GetMangaCoverImageUrl = (apiUri: string, mangaId: string, ref: HTMLImageElement | undefined | null) : string => {
|
||||||
if(ref == null || ref == undefined)
|
if(ref == null || ref == undefined)
|
||||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`;
|
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=64&height=64`;
|
||||||
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
|
return `${apiUri}/v2/Manga/${mangaId}/Cover?width=${ref.clientWidth}&height=${ref.clientHeight}`;
|
||||||
|
@ -2,7 +2,6 @@ import {deleteData, getData, putData} from "./fetchApi.tsx";
|
|||||||
import INotificationConnector from "./types/INotificationConnector.ts";
|
import INotificationConnector from "./types/INotificationConnector.ts";
|
||||||
import IGotifyRecord from "./types/records/IGotifyRecord.ts";
|
import IGotifyRecord from "./types/records/IGotifyRecord.ts";
|
||||||
import INtfyRecord from "./types/records/INtfyRecord.ts";
|
import INtfyRecord from "./types/records/INtfyRecord.ts";
|
||||||
import ILunaseaRecord from "./types/records/ILunaseaRecord.ts";
|
|
||||||
import IPushoverRecord from "./types/records/IPushoverRecord.ts";
|
import IPushoverRecord from "./types/records/IPushoverRecord.ts";
|
||||||
|
|
||||||
export const GetNotificationConnectors = async (apiUri: string) : Promise<INotificationConnector[]> => {
|
export const GetNotificationConnectors = async (apiUri: string) : Promise<INotificationConnector[]> => {
|
||||||
@ -39,12 +38,6 @@ export const CreateNtfy = async (apiUri: string, ntfy: INtfyRecord) : Promise<s
|
|||||||
return await putData(`${apiUri}/v2/NotificationConnector/Ntfy`, ntfy) as Promise<string>;
|
return await putData(`${apiUri}/v2/NotificationConnector/Ntfy`, ntfy) as Promise<string>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const CreateLunasea = async (apiUri: string, lunasea: ILunaseaRecord) : Promise<string> => {
|
|
||||||
if(lunasea === undefined || lunasea === null)
|
|
||||||
return Promise.reject("lunasea was not provided");
|
|
||||||
return await putData(`${apiUri}/v2/NotificationConnector/Lunasea`, lunasea) as Promise<string>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const CreatePushover = async (apiUri: string, pushover: IPushoverRecord) : Promise<string> => {
|
export const CreatePushover = async (apiUri: string, pushover: IPushoverRecord) : Promise<string> => {
|
||||||
if(pushover === undefined || pushover === null)
|
if(pushover === undefined || pushover === null)
|
||||||
return Promise.reject("pushover was not provided");
|
return Promise.reject("pushover was not provided");
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import {postData} from "./fetchApi.tsx";
|
import {getData, postData} from "./fetchApi.tsx";
|
||||||
import IManga from "./types/IManga.ts";
|
import IManga from "./types/IManga.ts";
|
||||||
|
|
||||||
export const SearchName = async (apiUri: string, name: string) : Promise<IManga[]> => {
|
export const SearchName = async (apiUri: string, name: string) : Promise<IManga[]> => {
|
||||||
@ -12,7 +12,7 @@ export const SearchNameOnConnector = async (apiUri: string, connectorName: strin
|
|||||||
return Promise.reject("connectorName was not provided");
|
return Promise.reject("connectorName was not provided");
|
||||||
if(name === undefined || name === null || name.length < 1)
|
if(name === undefined || name === null || name.length < 1)
|
||||||
return Promise.reject("name was not provided");
|
return Promise.reject("name was not provided");
|
||||||
return await postData(`${apiUri}/v2/Search/${connectorName}`, name) as Promise<IManga[]>;
|
return await getData(`${apiUri}/v2/Search/${connectorName}/${name}`) as Promise<IManga[]>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SearchUrl = async (apiUri: string, url: string) : Promise<IManga> => {
|
export const SearchUrl = async (apiUri: string, url: string) : Promise<IManga> => {
|
||||||
|
@ -1,4 +0,0 @@
|
|||||||
export enum LibraryType {
|
|
||||||
Komga = "Komga",
|
|
||||||
Kavita = "Kavita"
|
|
||||||
}
|
|
@ -1,10 +1,11 @@
|
|||||||
export default interface IChapter{
|
export default interface IChapter{
|
||||||
chapterId: string;
|
chapterId: string;
|
||||||
volumeNumber: number;
|
parentMangaId: string;
|
||||||
|
volumeNumber: number | null;
|
||||||
chapterNumber: string;
|
chapterNumber: string;
|
||||||
url: string;
|
url: string;
|
||||||
title: string | undefined;
|
title: string | null;
|
||||||
archiveFileName: string;
|
fileName: string | null;
|
||||||
downloaded: boolean;
|
downloaded: boolean;
|
||||||
parentMangaId: string;
|
fullArchiveFilePath: string;
|
||||||
}
|
}
|
@ -1,8 +1,11 @@
|
|||||||
import {LibraryType} from "./EnumLibraryType";
|
|
||||||
|
|
||||||
export default interface ILibraryConnector {
|
export default interface ILibraryConnector {
|
||||||
libraryConnectorId: string;
|
libraryConnectorId: string;
|
||||||
libraryType: LibraryType;
|
libraryType: LibraryType;
|
||||||
baseUrl: string;
|
baseUrl: string;
|
||||||
auth: string;
|
auth: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export enum LibraryType {
|
||||||
|
Komga = "Komga",
|
||||||
|
Kavita = "Kavita"
|
||||||
}
|
}
|
@ -1,4 +1,8 @@
|
|||||||
import {MangaReleaseStatus} from "./EnumMangaReleaseStatus";
|
import {MangaReleaseStatus} from "./EnumMangaReleaseStatus";
|
||||||
|
import IAuthor from "./IAuthor.ts";
|
||||||
|
import IMangaAltTitle from "./IMangaAltTitle.ts";
|
||||||
|
import IMangaTag from "./IMangaTag.ts";
|
||||||
|
import ILink from "./ILink.ts";
|
||||||
|
|
||||||
export default interface IManga{
|
export default interface IManga{
|
||||||
mangaId: string;
|
mangaId: string;
|
||||||
@ -6,16 +10,18 @@ export default interface IManga{
|
|||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
websiteUrl: string;
|
websiteUrl: string;
|
||||||
year: number;
|
|
||||||
originalLanguage: string;
|
|
||||||
releaseStatus: MangaReleaseStatus;
|
releaseStatus: MangaReleaseStatus;
|
||||||
folderName: string;
|
libraryId: string | null;
|
||||||
ignoreChapterBefore: number;
|
mangaConnectorName: string;
|
||||||
mangaConnectorId: string;
|
authors: IAuthor[] | null;
|
||||||
authorIds: string[];
|
mangaTags: IMangaTag[] | null;
|
||||||
tags: string[];
|
links: ILink[] | null;
|
||||||
linkIds: string[];
|
altTitles: IMangaAltTitle[] | null;
|
||||||
altTitleIds: string[];
|
ignoreChaptersBefore: number;
|
||||||
|
directoryName: string;
|
||||||
|
year: number | null;
|
||||||
|
originalLanguage: string | null;
|
||||||
|
chapterIds: string[] | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DefaultManga : IManga = {
|
export const DefaultManga : IManga = {
|
||||||
@ -24,14 +30,16 @@ export const DefaultManga : IManga = {
|
|||||||
name: "Loading",
|
name: "Loading",
|
||||||
description: "Loading",
|
description: "Loading",
|
||||||
websiteUrl: "",
|
websiteUrl: "",
|
||||||
|
releaseStatus: MangaReleaseStatus.Continuing,
|
||||||
|
libraryId: null,
|
||||||
|
mangaConnectorName: "Loading",
|
||||||
|
authors: null,
|
||||||
|
mangaTags: null,
|
||||||
|
links: null,
|
||||||
|
altTitles: null,
|
||||||
|
ignoreChaptersBefore: 0,
|
||||||
|
directoryName: "",
|
||||||
year: 1999,
|
year: 1999,
|
||||||
originalLanguage: "en",
|
originalLanguage: "en",
|
||||||
releaseStatus: MangaReleaseStatus.Continuing,
|
chapterIds: null
|
||||||
folderName: "Loading",
|
|
||||||
ignoreChapterBefore: 0,
|
|
||||||
mangaConnectorId: "Loading",
|
|
||||||
authorIds: ["Loading"],
|
|
||||||
tags: ["Loading"],
|
|
||||||
linkIds: ["Loading"],
|
|
||||||
altTitleIds: ["Loading"],
|
|
||||||
}
|
}
|
3
tranga-website/src/api/types/IMangaTag.ts
Normal file
3
tranga-website/src/api/types/IMangaTag.ts
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
export default interface IMangaTag {
|
||||||
|
tag: string;
|
||||||
|
}
|
@ -1,7 +1,6 @@
|
|||||||
export default interface IJob{
|
export default interface IJob{
|
||||||
jobId: string;
|
jobId: string;
|
||||||
parentJobId: string;
|
parentJobId: string | null;
|
||||||
dependsOnJobIds: string[];
|
|
||||||
jobType: JobType;
|
jobType: JobType;
|
||||||
recurrenceMs: number;
|
recurrenceMs: number;
|
||||||
lastExecution: Date;
|
lastExecution: Date;
|
||||||
@ -17,13 +16,15 @@ export enum JobType {
|
|||||||
MoveFileOrFolderJob = "MoveFileOrFolderJob",
|
MoveFileOrFolderJob = "MoveFileOrFolderJob",
|
||||||
DownloadMangaCoverJob = "DownloadMangaCoverJob",
|
DownloadMangaCoverJob = "DownloadMangaCoverJob",
|
||||||
RetrieveChaptersJob = "RetrieveChaptersJob",
|
RetrieveChaptersJob = "RetrieveChaptersJob",
|
||||||
UpdateFilesDownloadedJob = "UpdateFilesDownloadedJob",
|
UpdateChaptersDownloadedJob = "UpdateChaptersDownloadedJob",
|
||||||
MoveMangaLibraryJob = "MoveMangaLibraryJob"
|
MoveMangaLibraryJob = "MoveMangaLibraryJob",
|
||||||
|
UpdateSingleChapterDownloadedJob = "UpdateSingleChapterDownloadedJob"
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum JobState {
|
export enum JobState {
|
||||||
Waiting = "Waiting",
|
FirstExecution = "FirstExecution",
|
||||||
Running = "Running",
|
Running = "Running",
|
||||||
Completed = "Completed",
|
Completed = "Completed",
|
||||||
|
CompletedWaiting = "CompletedWaiting",
|
||||||
Failed = "Failed"
|
Failed = "Failed"
|
||||||
}
|
}
|
@ -0,0 +1,5 @@
|
|||||||
|
export default interface IDownloadAvailableChaptersJobRecord {
|
||||||
|
language: string;
|
||||||
|
recurrenceTimeMs: number;
|
||||||
|
localLibraryId: string;
|
||||||
|
}
|
@ -1,4 +0,0 @@
|
|||||||
export default interface IDownloadAvailableJobsRecord {
|
|
||||||
recurrenceTimeMs: number;
|
|
||||||
localLibraryId: string;
|
|
||||||
}
|
|
@ -1,5 +1,3 @@
|
|||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
|
|
||||||
export default interface IGotifyRecord {
|
export default interface IGotifyRecord {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
appToken: string;
|
appToken: string;
|
||||||
|
@ -1,5 +0,0 @@
|
|||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
|
|
||||||
export default interface ILunaseaRecord {
|
|
||||||
id: string;
|
|
||||||
}
|
|
@ -1,5 +1,3 @@
|
|||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
|
|
||||||
export default interface INtfyRecord {
|
export default interface INtfyRecord {
|
||||||
endpoint: string;
|
endpoint: string;
|
||||||
username: string;
|
username: string;
|
||||||
|
@ -1,5 +1,3 @@
|
|||||||
import "../../../styles/notificationConnector.css";
|
|
||||||
|
|
||||||
export default interface IPushoverRecord {
|
export default interface IPushoverRecord {
|
||||||
apptoken: string;
|
apptoken: string;
|
||||||
user: string;
|
user: string;
|
||||||
|
@ -6,11 +6,12 @@ import '@fontsource/inter';
|
|||||||
import { CssVarsProvider } from '@mui/joy/styles';
|
import { CssVarsProvider } from '@mui/joy/styles';
|
||||||
import CssBaseline from '@mui/joy/CssBaseline';
|
import CssBaseline from '@mui/joy/CssBaseline';
|
||||||
import {StrictMode} from "react";
|
import {StrictMode} from "react";
|
||||||
|
import {trangaTheme} from "./theme.ts";
|
||||||
|
|
||||||
export default function MyApp() {
|
export default function MyApp() {
|
||||||
return (
|
return (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<CssVarsProvider>
|
<CssVarsProvider theme={trangaTheme}>
|
||||||
{/* must be used under CssVarsProvider */}
|
{/* must be used under CssVarsProvider */}
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
|
||||||
|
88
tranga-website/src/theme.ts
Normal file
88
tranga-website/src/theme.ts
Normal file
@ -0,0 +1,88 @@
|
|||||||
|
import { extendTheme } from '@mui/joy/styles';
|
||||||
|
|
||||||
|
|
||||||
|
export const trangaTheme = extendTheme({
|
||||||
|
"colorSchemes": {
|
||||||
|
"light": {
|
||||||
|
"palette": {
|
||||||
|
"primary": {
|
||||||
|
"50": "#FCE5EA",
|
||||||
|
"100": "#FBDDE3",
|
||||||
|
"200": "#F9CBD4",
|
||||||
|
"300": "#F7BAC6",
|
||||||
|
"400": "#F5A9B8",
|
||||||
|
"500": "#F5A9B8",
|
||||||
|
"600": "#C48793",
|
||||||
|
"700": "#AC7681",
|
||||||
|
"800": "#93656E",
|
||||||
|
"900": "#7B555C"
|
||||||
|
},
|
||||||
|
"neutral": {
|
||||||
|
"50": "#E6E6E6",
|
||||||
|
"100": "#CCCCCC",
|
||||||
|
"200": "#B3B3B3",
|
||||||
|
"300": "#999999",
|
||||||
|
"400": "#808080",
|
||||||
|
"500": "#666666",
|
||||||
|
"600": "#4C4C4C",
|
||||||
|
"700": "#333333",
|
||||||
|
"800": "#191919",
|
||||||
|
"900": "#000",
|
||||||
|
"plainColor": "var(--joy-palette-neutral-50)",
|
||||||
|
"plainHoverBg": "var(--joy-palette-neutral-700)",
|
||||||
|
"outlinedColor": "var(--joy-palette-neutral-50)",
|
||||||
|
},
|
||||||
|
"success": {
|
||||||
|
"50": "#cef0fe",
|
||||||
|
"100": "#bdebfd",
|
||||||
|
"200": "#9de2fc",
|
||||||
|
"300": "#7cd8fb",
|
||||||
|
"400": "#5bcefa",
|
||||||
|
"500": "#5bcefa",
|
||||||
|
"600": "#49a5c8",
|
||||||
|
"700": "#4090af",
|
||||||
|
"800": "#2e677d",
|
||||||
|
"900": "#245264"
|
||||||
|
},
|
||||||
|
"danger": {
|
||||||
|
"50": "#f2c0b3",
|
||||||
|
"100": "#ea9680",
|
||||||
|
"200": "#e68166",
|
||||||
|
"300": "#dd5733",
|
||||||
|
"400": "#d52d00",
|
||||||
|
"500": "#d52d00",
|
||||||
|
"600": "#aa2400",
|
||||||
|
"700": "#951f00",
|
||||||
|
"800": "#6b1700",
|
||||||
|
"900": "#400d00"
|
||||||
|
},
|
||||||
|
"warning": {
|
||||||
|
"50": "#ffebdd",
|
||||||
|
"100": "#ffd7bb",
|
||||||
|
"200": "#ffc29a",
|
||||||
|
"300": "#ffae78",
|
||||||
|
"400": "#ff9a56",
|
||||||
|
"500": "#ff9a56",
|
||||||
|
"600": "#cc7b45",
|
||||||
|
"700": "#995c34",
|
||||||
|
"800": "#663e22",
|
||||||
|
"900": "#331f11"
|
||||||
|
},
|
||||||
|
"background": {
|
||||||
|
"body": "var(--joy-palette-neutral-900)",
|
||||||
|
"surface": "var(--joy-palette-neutral-900)",
|
||||||
|
"popup": "var(--joy-palette-neutral-800)"
|
||||||
|
},
|
||||||
|
"text": {
|
||||||
|
"primary": "var(--joy-palette-neutral-50)",
|
||||||
|
"secondary": "var(--joy-palette-success-200)",
|
||||||
|
"tertiary": "var(--joy-palette-primary-200)",
|
||||||
|
"icon": "var(--joy-palette-primary-50)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"dark": {
|
||||||
|
"palette": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
Loading…
x
Reference in New Issue
Block a user