mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-05-07 15:42:10 +02:00
Compare commits
5 Commits
6b10aa8926
...
6d402357e7
Author | SHA1 | Date | |
---|---|---|---|
6d402357e7 | |||
9e0eb0262e | |||
63b220bd79 | |||
d480f62e51 | |||
44755675e5 |
@ -2,15 +2,17 @@ import Sheet from '@mui/joy/Sheet';
|
||||
import './App.css'
|
||||
import Settings from "./Settings.tsx";
|
||||
import Header from "./Header.tsx";
|
||||
import {Button} from "@mui/joy";
|
||||
import {Badge, Button} from "@mui/joy";
|
||||
import {useState} from "react";
|
||||
import {ApiUriContext} from "./api/fetchApi.tsx";
|
||||
import Search from './Components/Search.tsx';
|
||||
import MangaList from "./Components/MangaList.tsx";
|
||||
|
||||
export default function App () {
|
||||
|
||||
const [showSettings, setShowSettings] = useState<boolean>(false);
|
||||
const [showSearch, setShowSearch] = useState<boolean>(false);
|
||||
const [apiConnected, setApiConnected] = useState<boolean>(false);
|
||||
|
||||
const [apiUri, setApiUri] = useState<string>(window.location.href.substring(0, window.location.href.lastIndexOf("/")));
|
||||
|
||||
@ -18,12 +20,15 @@ export default function App () {
|
||||
<ApiUriContext.Provider value={apiUri}>
|
||||
<Sheet className={"app"}>
|
||||
<Header>
|
||||
<Badge color={"danger"} invisible={apiConnected} badgeContent={"!"}>
|
||||
<Button onClick={() => setShowSettings(true)}>Settings</Button>
|
||||
</Badge>
|
||||
<Button onClick={() => setShowSearch(true)}>Search</Button>
|
||||
</Header>
|
||||
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri}/>
|
||||
<Sheet className={"app-content"}>
|
||||
<Settings open={showSettings} setOpen={setShowSettings} setApiUri={setApiUri} setConnected={setApiConnected} />
|
||||
<Search open={showSearch} setOpen={setShowSearch} />
|
||||
<Sheet className={"app-content"}>
|
||||
<MangaList />
|
||||
</Sheet>
|
||||
</Sheet>
|
||||
</ApiUriContext.Provider>
|
||||
|
@ -13,7 +13,7 @@ import {
|
||||
} from "@mui/joy";
|
||||
import IManga, {DefaultManga} from "../api/types/IManga.ts";
|
||||
import {ReactElement, useCallback, useContext, useEffect, useState} from "react";
|
||||
import {GetLatestChapterAvailable, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
|
||||
import {GetLatestChapterAvailable, GetMangaById, GetMangaCoverImageUrl, SetIgnoreThreshold} from "../api/Manga.tsx";
|
||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||
import AuthorTag from "./AuthorTag.tsx";
|
||||
import LinkTag from "./LinkTag.tsx";
|
||||
@ -22,8 +22,27 @@ import IChapter from "../api/types/IChapter.ts";
|
||||
import MarkdownPreview from "@uiw/react-markdown-preview";
|
||||
import {SxProps} from "@mui/joy/styles/types";
|
||||
|
||||
export function Manga({manga, children} : { manga: IManga | undefined, 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 [loading, setLoading] = useState(true);
|
||||
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
|
||||
const loadManga = useCallback(() => {
|
||||
setLoading(true);
|
||||
GetMangaById(apiUri, mangaId).then(setManga).finally(() => setLoading(false));
|
||||
},[apiUri, mangaId]);
|
||||
|
||||
useEffect(() => {
|
||||
loadManga();
|
||||
}, []);
|
||||
|
||||
return <Manga manga={manga} loading={loading} children={children} />
|
||||
}
|
||||
|
||||
export function Manga({manga, children, loading} : { manga: IManga | undefined, children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined, loading?: boolean}) {
|
||||
const useManga = manga ?? DefaultManga;
|
||||
loading = loading ?? false;
|
||||
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
|
||||
@ -59,7 +78,7 @@ export function Manga({manga, children} : { manga: IManga | undefined, children?
|
||||
position: "relative",
|
||||
}
|
||||
|
||||
const interactiveElements = ["button", "input", "textarea", "a"];
|
||||
const interactiveElements = ["button", "input", "textarea", "a", "select", "option", "li"];
|
||||
|
||||
return (
|
||||
<Badge badgeContent={useManga.mangaConnectorId} color={ReleaseStatusToPalette(useManga.releaseStatus)} size={"lg"}>
|
||||
@ -79,19 +98,25 @@ export function Manga({manga, children} : { manga: IManga | undefined, children?
|
||||
}}/>
|
||||
<CardContent sx={{display: "flex", alignItems: "center", flexFlow: "row nowrap"}}>
|
||||
<Box sx={sideSx}>
|
||||
<Skeleton loading={loading}>
|
||||
<Link href={useManga.websiteUrl} level={"h1"} sx={{height:"min-content",width:"fit-content",color:"white",margin:"0 0 0 10px"}}>
|
||||
{useManga.name}
|
||||
</Link>
|
||||
</Skeleton>
|
||||
</Box>
|
||||
{
|
||||
expanded ?
|
||||
<Box sx={sideSx}>
|
||||
<Skeleton loading={loading} variant={"text"} level={"title-lg"}>
|
||||
<Stack direction={"row"} flexWrap={"wrap"} spacing={0.5}>
|
||||
{useManga.authorIds.map(authorId => <AuthorTag key={authorId} authorId={authorId} color={"success"} />)}
|
||||
{useManga.tags.map(tag => <Chip key={tag} variant={"outlined"} size={"md"} color={"primary"}>{tag}</Chip>)}
|
||||
{useManga.linkIds.map(linkId => <LinkTag key={linkId} linkId={linkId} color={"danger"} />)}
|
||||
</Stack>
|
||||
</Skeleton>
|
||||
<Skeleton loading={loading} sx={{maxHeight:"300px"}}>
|
||||
<MarkdownPreview source={useManga.description} style={{backgroundColor: "transparent", color: "black"}} />
|
||||
</Skeleton>
|
||||
</Box>
|
||||
: null
|
||||
}
|
||||
@ -99,6 +124,7 @@ export function Manga({manga, children} : { manga: IManga | undefined, children?
|
||||
{
|
||||
expanded ?
|
||||
<CardActions sx={{justifyContent:"space-between"}}>
|
||||
<Skeleton loading={loading} sx={{maxHeight: "30px", maxWidth:"calc(100% - 40px)"}}>
|
||||
<Input
|
||||
type={"number"}
|
||||
placeholder={"0.0"}
|
||||
@ -123,6 +149,7 @@ export function Manga({manga, children} : { manga: IManga | undefined, children?
|
||||
onChange={(e) => updateIgnoreThreshhold(e.currentTarget.valueAsNumber)}
|
||||
/>
|
||||
{children}
|
||||
</Skeleton>
|
||||
</CardActions>
|
||||
: null
|
||||
}
|
||||
|
36
tranga-website/src/Components/MangaList.tsx
Normal file
36
tranga-website/src/Components/MangaList.tsx
Normal file
@ -0,0 +1,36 @@
|
||||
import {Button, Stack} from "@mui/joy";
|
||||
import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {ApiUriContext} from "../api/fetchApi.tsx";
|
||||
import {DeleteJob, GetJobsWithType} from "../api/Job.tsx";
|
||||
import {JobType} from "../api/types/Jobs/IJob.ts";
|
||||
import IDownloadAvailableChaptersJob from "../api/types/Jobs/IDownloadAvailableChaptersJob.ts";
|
||||
import {MangaFromId} from "./Manga.tsx";
|
||||
import { Remove } from "@mui/icons-material";
|
||||
|
||||
export default function MangaList(){
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
|
||||
const [jobList, setJobList] = useState<IDownloadAvailableChaptersJob[]>([]);
|
||||
|
||||
const getJobList = useCallback(() => {
|
||||
GetJobsWithType(apiUri, JobType.DownloadAvailableChaptersJob).then((jl) => setJobList(jl as IDownloadAvailableChaptersJob[]));
|
||||
},[apiUri]);
|
||||
|
||||
const deleteJob = useCallback((jobId: string) => {
|
||||
DeleteJob(apiUri, jobId).finally(() => getJobList());
|
||||
},[apiUri]);
|
||||
|
||||
useEffect(() => {
|
||||
getJobList();
|
||||
}, [apiUri]);
|
||||
|
||||
return(
|
||||
<Stack direction="row" spacing={1}>
|
||||
{jobList.map((job) => (
|
||||
<MangaFromId key={job.mangaId} mangaId={job.mangaId}>
|
||||
<Button color={"danger"} endDecorator={<Remove />} onClick={() => deleteJob(job.jobId)}>Delete</Button>
|
||||
</MangaFromId>
|
||||
))}
|
||||
</Stack>
|
||||
);
|
||||
}
|
@ -1,15 +1,19 @@
|
||||
import {
|
||||
Avatar, Button, Chip,
|
||||
Avatar,
|
||||
Button,
|
||||
Chip,
|
||||
CircularProgress,
|
||||
Drawer,
|
||||
Input,
|
||||
ListItemDecorator,
|
||||
Option,
|
||||
Select,
|
||||
SelectOption,
|
||||
Skeleton, Stack,
|
||||
Select, SelectOption,
|
||||
Skeleton,
|
||||
Stack,
|
||||
Step,
|
||||
StepIndicator,
|
||||
Stepper, Typography
|
||||
Stepper,
|
||||
Typography
|
||||
} from "@mui/joy";
|
||||
import ModalClose from "@mui/joy/ModalClose";
|
||||
import IMangaConnector from "../api/types/IMangaConnector";
|
||||
@ -22,6 +26,9 @@ import {Manga} from "./Manga.tsx";
|
||||
import Add from "@mui/icons-material/Add";
|
||||
import React from "react";
|
||||
import {CreateDownloadAvailableChaptersJob} from "../api/Job.tsx";
|
||||
import ILocalLibrary from "../api/types/ILocalLibrary.ts";
|
||||
import {GetLibraries} from "../api/LocalLibrary.tsx";
|
||||
import { LibraryBooks } from "@mui/icons-material";
|
||||
|
||||
export default function Search({open, setOpen}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>}){
|
||||
|
||||
@ -32,10 +39,10 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
||||
const [mangaConnectorsLoading, setMangaConnectorsLoading] = useState<boolean>(true);
|
||||
const [selectedMangaConnector, setSelectedMangaConnector] = useState<IMangaConnector>();
|
||||
|
||||
useEffect(() => {
|
||||
const loadMangaConnectors = useCallback(() => {
|
||||
setMangaConnectorsLoading(true);
|
||||
GetAllConnectors(apiUri).then(setMangaConnectors).finally(() => setMangaConnectorsLoading(false));
|
||||
},[apiUri])
|
||||
}, [apiUri]);
|
||||
|
||||
const [results, setResults] = useState<IManga[]>([]);
|
||||
const [resultsLoading, setResultsLoading] = useState<boolean>(false);
|
||||
@ -48,6 +55,25 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
||||
SearchNameOnConnector(apiUri, mangaConnector.name, value).then(setResults).finally(() => setResultsLoading(false));
|
||||
},[apiUri])
|
||||
|
||||
const [localLibraries, setLocalLibraries] = useState<ILocalLibrary[]>();
|
||||
const [localLibrariesLoading, setLocalLibrariesLoading] = useState<boolean>(true);
|
||||
const [selectedLibraryId, setSelectedLibraryId] = useState<string>();
|
||||
|
||||
const loadLocalLibraries = useCallback(() => {
|
||||
setLocalLibrariesLoading(true);
|
||||
GetLibraries(apiUri).then(setLocalLibraries).finally(() => setLocalLibrariesLoading(false));
|
||||
}, [apiUri]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMangaConnectors();
|
||||
loadLocalLibraries();
|
||||
},[apiUri]);
|
||||
|
||||
useEffect(() => {
|
||||
loadMangaConnectors();
|
||||
loadLocalLibraries();
|
||||
}, []);
|
||||
|
||||
function renderValue(option: SelectOption<string> | null) {
|
||||
if (!option) {
|
||||
return null;
|
||||
@ -63,6 +89,7 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
||||
);
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
return (
|
||||
<Drawer size={"lg"} anchor={"right"} open={open} onClose={() => {
|
||||
setStep(2);
|
||||
@ -113,13 +140,28 @@ export default function Search({open, setOpen}:{open:boolean, setOpen:React.Disp
|
||||
<StepIndicator variant="solid" color="primary">
|
||||
3
|
||||
</StepIndicator>}>
|
||||
<Typography>Results</Typography>
|
||||
<Typography endDecorator={<Chip size={"sm"} color={"primary"}>{results.length}</Chip>}>Results</Typography>
|
||||
<Skeleton loading={resultsLoading}>
|
||||
<Stack direction={"row"} spacing={1}>
|
||||
{results.map((result) =>
|
||||
<Manga key={result.mangaId} manga={result}>
|
||||
<Button onClick={() => {
|
||||
CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: "",recurrenceTimeMs: 1000 * 60 * 60 * 3})
|
||||
<Select
|
||||
placeholder={"Select Library"}
|
||||
defaultValue={""}
|
||||
startDecorator={<LibraryBooks />}
|
||||
value={selectedLibraryId}
|
||||
onChange={(_e, newValue) => setSelectedLibraryId(newValue!)}>
|
||||
{localLibrariesLoading ?
|
||||
<Option value={""} disabled>Loading <CircularProgress color={"primary"} size={"sm"} /></Option>
|
||||
:
|
||||
(localLibraries??[]).map(library => {
|
||||
return (
|
||||
<Option value={library.localLibraryId}>{library.libraryName} ({library.basePath})</Option>
|
||||
);
|
||||
})}
|
||||
</Select>
|
||||
<Button disabled={localLibrariesLoading || selectedLibraryId === undefined} onClick={() => {
|
||||
CreateDownloadAvailableChaptersJob(apiUri, result.mangaId, {localLibraryId: selectedLibraryId!,recurrenceTimeMs: 1000 * 60 * 60 * 3})
|
||||
}} endDecorator={<Add />}>Watch</Button>
|
||||
</Manga>)}
|
||||
</Stack>
|
||||
|
38
tranga-website/src/Components/Settings/UserAgent.tsx
Normal file
38
tranga-website/src/Components/Settings/UserAgent.tsx
Normal file
@ -0,0 +1,38 @@
|
||||
import IBackendSettings from "../../api/types/IBackendSettings";
|
||||
import {Accordion, AccordionDetails, AccordionSummary, CircularProgress, ColorPaletteProp, Input} from "@mui/joy";
|
||||
import {KeyboardEventHandler, useCallback, useContext, useState} from "react";
|
||||
import {ApiUriContext} from "../../api/fetchApi.tsx";
|
||||
import {UpdateUserAgent} from "../../api/BackendSettings.tsx";
|
||||
|
||||
export default function UserAgent({backendSettings}: {backendSettings?: IBackendSettings}) {
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
const [loading, setLoading] = useState<boolean>(false);
|
||||
const [value, setValue] = useState<string>("");
|
||||
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
||||
|
||||
const keyDown : KeyboardEventHandler<HTMLInputElement> = useCallback((e) => {
|
||||
if(e.key === "Enter") {
|
||||
setLoading(true);
|
||||
UpdateUserAgent(apiUri, value)
|
||||
.then(() => setColor("success"))
|
||||
.catch(() => setColor("danger"))
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
}, [apiUri])
|
||||
|
||||
return (
|
||||
<Accordion>
|
||||
<AccordionSummary>UserAgent</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Input disabled={backendSettings === undefined || loading}
|
||||
placeholder={"UserAgent"}
|
||||
defaultValue={backendSettings?.userAgent}
|
||||
onKeyDown={keyDown}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
color={color}
|
||||
endDecorator={(loading ? <CircularProgress color={"primary"} size={"sm"} /> : null)}
|
||||
/>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
@ -10,8 +10,11 @@ import {
|
||||
} from "@mui/joy";
|
||||
import './Settings.css';
|
||||
import * as React from "react";
|
||||
import {useContext, useEffect, useState} from "react";
|
||||
import {useCallback, useContext, useEffect, useState} from "react";
|
||||
import {ApiUriContext} from "./api/fetchApi.tsx";
|
||||
import IBackendSettings from "./api/types/IBackendSettings.ts";
|
||||
import { GetSettings } from './api/BackendSettings.tsx';
|
||||
import UserAgent from "./Components/Settings/UserAgent.tsx";
|
||||
|
||||
const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||
return fetch(`${apiUri}/swagger/v2/swagger.json`,
|
||||
@ -28,7 +31,7 @@ const checkConnection = async (apiUri: string): Promise<boolean> =>{
|
||||
});
|
||||
}
|
||||
|
||||
export default function Settings({open, setOpen, setApiUri}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>, setApiUri:React.Dispatch<React.SetStateAction<string>>}) {
|
||||
export default function Settings({open, setOpen, setApiUri, setConnected}:{open:boolean, setOpen:React.Dispatch<React.SetStateAction<boolean>>, setApiUri:React.Dispatch<React.SetStateAction<string>>, setConnected:React.Dispatch<React.SetStateAction<boolean>>}) {
|
||||
|
||||
const apiUri = useContext(ApiUriContext);
|
||||
|
||||
@ -53,6 +56,7 @@ export default function Settings({open, setOpen, setApiUri}:{open:boolean, setOp
|
||||
setChecking(true);
|
||||
checkConnection(uri)
|
||||
.then((result) => {
|
||||
setConnected(result);
|
||||
setApiUriAccordionOpen(!result);
|
||||
setApiUriColor(result ? "success" : "danger");
|
||||
if(result)
|
||||
@ -61,6 +65,16 @@ export default function Settings({open, setOpen, setApiUri}:{open:boolean, setOp
|
||||
.finally(() => setChecking(false));
|
||||
}
|
||||
|
||||
const [backendSettings, setBackendSettings] = useState<IBackendSettings>();
|
||||
|
||||
const getBackendSettings = useCallback(() => {
|
||||
GetSettings(apiUri).then(setBackendSettings);
|
||||
}, [apiUri]);
|
||||
|
||||
useEffect(() => {
|
||||
getBackendSettings();
|
||||
}, [checking]);
|
||||
|
||||
return (
|
||||
<Drawer size={"md"} open={open} onClose={() => setOpen(false)}>
|
||||
<ModalClose />
|
||||
@ -86,6 +100,7 @@ export default function Settings({open, setOpen, setApiUri}:{open:boolean, setOp
|
||||
endDecorator={(checking ? <CircularProgress color={apiUriColor} size={"sm"} /> : null)} />
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
<UserAgent backendSettings={backendSettings} />
|
||||
</AccordionGroup>
|
||||
</DialogContent>
|
||||
</Drawer>
|
||||
|
@ -19,19 +19,19 @@ export default interface IManga{
|
||||
}
|
||||
|
||||
export const DefaultManga : IManga = {
|
||||
mangaId: "MangaId",
|
||||
idOnConnectorSite: "ID",
|
||||
name: "TestManga",
|
||||
description: "Wow so much text, very cool",
|
||||
websiteUrl: "https://realsite.realdomain",
|
||||
mangaId: "Loading",
|
||||
idOnConnectorSite: "Loading",
|
||||
name: "Loading",
|
||||
description: "Loading",
|
||||
websiteUrl: "",
|
||||
year: 1999,
|
||||
originalLanguage: "lindtChoccy",
|
||||
originalLanguage: "en",
|
||||
releaseStatus: MangaReleaseStatus.Continuing,
|
||||
folderName: "uhhh",
|
||||
folderName: "Loading",
|
||||
ignoreChapterBefore: 0,
|
||||
mangaConnectorId: "MangaDex",
|
||||
authorIds: ["We got", "Authors"],
|
||||
tags: ["And we", "got Tags"],
|
||||
linkIds: ["And most", "definitely", "links"],
|
||||
altTitleIds: ["But not alt-titles."],
|
||||
mangaConnectorId: "Loading",
|
||||
authorIds: ["Loading"],
|
||||
tags: ["Loading"],
|
||||
linkIds: ["Loading"],
|
||||
altTitleIds: ["Loading"],
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user