mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-09-10 11:58:20 +02:00
Add ChaptersSection to Manga, add TCheckbox and rework other custom components
This commit is contained in:
@@ -70,7 +70,7 @@ export function App() {
|
||||
setOpen={setDownloadDrawerOpen}
|
||||
mangaKey={selectedMangaKey}
|
||||
downloadOpen={downloadSectionOpen}>
|
||||
<TButton completionAction={() => removeManga(selectedMangaKey)}>
|
||||
<TButton onClick={() => removeManga(selectedMangaKey)}>
|
||||
Remove
|
||||
</TButton>
|
||||
</MangaDetail>
|
||||
|
@@ -8,9 +8,9 @@ export default function TButton(props: TButtonProps) {
|
||||
const clicked: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||
setState(TState.busy);
|
||||
e.preventDefault();
|
||||
if (props.completionAction)
|
||||
if (props.onClick)
|
||||
props
|
||||
.completionAction(undefined)
|
||||
.onClick()
|
||||
.then(() => setState(TState.success))
|
||||
.catch(() => setState(TState.failure));
|
||||
};
|
||||
@@ -29,4 +29,5 @@ export default function TButton(props: TButtonProps) {
|
||||
|
||||
export interface TButtonProps extends TProps {
|
||||
children?: ReactNode;
|
||||
onClick?: () => Promise<void>;
|
||||
}
|
||||
|
35
tranga-website/src/Components/Inputs/TCheckbox.tsx
Normal file
35
tranga-website/src/Components/Inputs/TCheckbox.tsx
Normal file
@@ -0,0 +1,35 @@
|
||||
import { Checkbox } from '@mui/joy';
|
||||
import TProps, { TColor, TDisabled, TState } from './TProps.ts';
|
||||
import { ChangeEvent, ReactNode, useState } from 'react';
|
||||
|
||||
export default function TCheckbox(props: TCheckboxProps) {
|
||||
const [state, setState] = useState<TState>(TState.clean);
|
||||
|
||||
const onChange = (e: ChangeEvent<HTMLInputElement>) => {
|
||||
setState(TState.busy);
|
||||
e.preventDefault();
|
||||
if (props.onCheckChanged)
|
||||
props
|
||||
.onCheckChanged(e.target.checked)
|
||||
.then(() => setState(TState.success))
|
||||
.catch(() => setState(TState.failure));
|
||||
};
|
||||
|
||||
return (
|
||||
<Checkbox
|
||||
color={TColor(state)}
|
||||
disabled={props.disabled ?? TDisabled(state)}
|
||||
aria-disabled={props.disabled ?? TDisabled(state)}
|
||||
onChange={onChange}
|
||||
className={'t-loadable'}
|
||||
defaultChecked={props.defaultChecked}
|
||||
label={props.label}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface TCheckboxProps extends TProps {
|
||||
label?: ReactNode;
|
||||
defaultChecked?: boolean;
|
||||
onCheckChanged?: (value: boolean) => Promise<void>;
|
||||
}
|
@@ -6,12 +6,10 @@ import './loadingBorder.css';
|
||||
|
||||
export default function TInput(props: TInputProps) {
|
||||
const [state, setState] = useState<TState>(TState.clean);
|
||||
const [value, setValue] = useState<string | number | readonly string[] | undefined>(
|
||||
const [value, setValue] = useState<string | number | undefined>(props.defaultValue);
|
||||
const [initialValue, setInitialValue] = useState<string | number | undefined>(
|
||||
props.defaultValue
|
||||
);
|
||||
const [initialValue, setInitialValue] = useState<
|
||||
string | number | readonly string[] | undefined
|
||||
>(props.defaultValue);
|
||||
|
||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
||||
|
||||
@@ -39,9 +37,9 @@ export default function TInput(props: TInputProps) {
|
||||
const submit = () => {
|
||||
setState(TState.busy);
|
||||
clearTimeout(timerRef.current);
|
||||
if (props.completionAction)
|
||||
if (props.onSubmit)
|
||||
props
|
||||
.completionAction(value)
|
||||
.onSubmit(value)
|
||||
.then(() => {
|
||||
setState(TState.success);
|
||||
setInitialValue(value);
|
||||
@@ -82,9 +80,10 @@ export default function TInput(props: TInputProps) {
|
||||
|
||||
export interface TInputProps extends TProps {
|
||||
placeholder?: string;
|
||||
defaultValue?: string | number | readonly string[];
|
||||
defaultValue?: string | number;
|
||||
actionDelay?: number;
|
||||
autoSubmit?: boolean;
|
||||
submitButtonHidden?: boolean;
|
||||
submitButtonText?: string;
|
||||
onSubmit?: (value?: string | number) => Promise<void>;
|
||||
}
|
||||
|
@@ -34,5 +34,4 @@ export const TColor = (state: TState): ColorPaletteProp => {
|
||||
|
||||
export default interface TProps {
|
||||
disabled?: boolean;
|
||||
completionAction?: (value?: string | number | readonly string[]) => Promise<void>;
|
||||
}
|
||||
|
@@ -0,0 +1,78 @@
|
||||
import { ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import { Chapter, Manga, MangaConnector, MangaConnectorId } from '../../../api/data-contracts.ts';
|
||||
import { Accordion, AccordionDetails, AccordionSummary, Table, Typography } from '@mui/joy';
|
||||
import { ApiContext } from '../../../contexts/ApiContext.tsx';
|
||||
import { MangaConnectorContext } from '../../../contexts/MangaConnectorContext.tsx';
|
||||
import MangaConnectorIcon from '../MangaConnectorIcon.tsx';
|
||||
import TCheckbox from '../../Inputs/TCheckbox.tsx';
|
||||
|
||||
export default function ChaptersSection(props: ChaptersSectionProps): ReactNode {
|
||||
const Api = useContext(ApiContext);
|
||||
const MangaConnectors = useContext(MangaConnectorContext);
|
||||
|
||||
const [chapters, setChapters] = useState<Chapter[]>([]);
|
||||
useEffect(() => {
|
||||
if (!props.manga) return;
|
||||
Api.mangaChaptersList(props.manga.key).then((data) => {
|
||||
if (!data.ok) return;
|
||||
setChapters(data.data);
|
||||
});
|
||||
}, [props]);
|
||||
|
||||
const chapterConnectorCheckbox = (chapter: Chapter, connector: MangaConnector): ReactNode => {
|
||||
const id = chapter.mangaConnectorIds.find((id) => id.mangaConnectorName == connector.key);
|
||||
if (!id) return null;
|
||||
return (
|
||||
<TCheckbox
|
||||
onCheckChanged={(value) => setDownloadingFrom(id, value)}
|
||||
defaultChecked={id.useForDownload}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const setDownloadingFrom = (id: MangaConnectorId, value: boolean): Promise<void> => {
|
||||
return Promise.reject();
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion sx={{ maxHeight: '50vh' }}>
|
||||
<AccordionSummary>
|
||||
<Typography level={'h3'}>Chapters</Typography>
|
||||
</AccordionSummary>
|
||||
<AccordionDetails>
|
||||
<Typography level={'body-md'}>Set source for chapter</Typography>
|
||||
<Table>
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Vol</th>
|
||||
<th>Ch</th>
|
||||
<th>Title</th>
|
||||
{MangaConnectors.map((con) => (
|
||||
<th>
|
||||
<MangaConnectorIcon mangaConnector={con} />
|
||||
{con.name}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{chapters.map((ch) => (
|
||||
<tr>
|
||||
<td>{ch.volume}</td>
|
||||
<td>{ch.chapterNumber}</td>
|
||||
<td>{ch.title}</td>
|
||||
{MangaConnectors.map((con) => (
|
||||
<td>{chapterConnectorCheckbox(ch, con)}</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</Table>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
);
|
||||
}
|
||||
|
||||
export interface ChaptersSectionProps {
|
||||
manga?: Manga;
|
||||
}
|
||||
|
@@ -11,15 +11,61 @@ import {
|
||||
Stack,
|
||||
Typography,
|
||||
} from '@mui/joy';
|
||||
import { ReactNode, useContext } from 'react';
|
||||
import { ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import TButton from '../../Inputs/TButton.tsx';
|
||||
import MangaConnectorIcon from '../MangaConnectorIcon.tsx';
|
||||
import { FileLibrary, Manga, MangaConnectorId } from '../../../api/data-contracts.ts';
|
||||
import { FileLibraryContext } from '../../../contexts/FileLibraryContext.tsx';
|
||||
import { ApiContext } from '../../../contexts/ApiContext.tsx';
|
||||
|
||||
export default function DownloadSection(props: DownloadSectionProps): ReactNode {
|
||||
export function DownloadSection(props: DownloadSectionProps): ReactNode {
|
||||
const Api = useContext(ApiContext);
|
||||
const Libraries = useContext(FileLibraryContext);
|
||||
|
||||
const [manga, setManga] = useState<Manga>();
|
||||
const [library, setLibrary] = useState<FileLibrary | undefined>();
|
||||
const [downloadFromMap, setDownloadFromMap] = useState<Map<MangaConnectorId, boolean>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
const newMap = new Map();
|
||||
setLibrary(Libraries.find((library) => library.key == manga?.fileLibraryId));
|
||||
manga?.mangaConnectorIds.forEach((id) => {
|
||||
newMap.set(id, id.useForDownload);
|
||||
});
|
||||
setDownloadFromMap(newMap);
|
||||
}, [manga, Libraries]);
|
||||
|
||||
useEffect(() => {
|
||||
setManga(props.manga);
|
||||
}, [props]);
|
||||
|
||||
const onLibraryChange = (_: any, value: string | null) => {
|
||||
setLibrary(Libraries.find((library) => library.key == value));
|
||||
};
|
||||
|
||||
const setDownload = async (): Promise<void> => {
|
||||
if (!manga) return Promise.reject();
|
||||
if (library) {
|
||||
const s = await Api.mangaChangeLibraryCreate(manga.key, library?.key)
|
||||
.then((result) => result.ok)
|
||||
.catch(() => false);
|
||||
if (!s) return Promise.reject();
|
||||
}
|
||||
for (const kv of downloadFromMap) {
|
||||
const s = await Api.mangaSetAsDownloadFromCreate(
|
||||
manga?.key,
|
||||
kv[0].mangaConnectorName,
|
||||
kv[1]
|
||||
)
|
||||
.then((result) => result.ok)
|
||||
.catch(() => false);
|
||||
if (!s) return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
return (
|
||||
<Accordion defaultExpanded={props.downloadOpen}>
|
||||
<AccordionSummary>
|
||||
@@ -34,8 +80,8 @@ export default function DownloadSection(props: DownloadSectionProps): ReactNode
|
||||
<Typography>Select a Library to Download to:</Typography>
|
||||
<Select
|
||||
placeholder={'Select a Library'}
|
||||
value={props.library?.key}
|
||||
onChange={props.onLibraryChange}>
|
||||
value={library?.key}
|
||||
onChange={onLibraryChange}>
|
||||
{Libraries.map((l) => (
|
||||
<Option
|
||||
key={l.key}
|
||||
@@ -54,9 +100,7 @@ export default function DownloadSection(props: DownloadSectionProps): ReactNode
|
||||
<ListItem key={id.key}>
|
||||
<Checkbox
|
||||
defaultChecked={id.useForDownload}
|
||||
onChange={(c) =>
|
||||
props.downloadFromMap.set(id, c.target.checked)
|
||||
}
|
||||
onChange={(c) => downloadFromMap.set(id, c.target.checked)}
|
||||
label={
|
||||
<div
|
||||
style={{
|
||||
@@ -75,7 +119,7 @@ export default function DownloadSection(props: DownloadSectionProps): ReactNode
|
||||
))}
|
||||
</List>
|
||||
</Box>
|
||||
<TButton completionAction={props.setDownload}>Download All</TButton>
|
||||
<TButton onClick={setDownload}>Download All</TButton>
|
||||
</Stack>
|
||||
</AccordionDetails>
|
||||
</Accordion>
|
||||
@@ -84,8 +128,4 @@ export default function DownloadSection(props: DownloadSectionProps): ReactNode
|
||||
export interface DownloadSectionProps {
|
||||
manga?: Manga;
|
||||
downloadOpen: boolean;
|
||||
library?: FileLibrary;
|
||||
onLibraryChange: (_: any, value: string | null) => void;
|
||||
downloadFromMap: Map<MangaConnectorId, boolean>;
|
||||
setDownload: () => Promise<void>;
|
||||
}
|
||||
|
@@ -1,67 +1,27 @@
|
||||
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react';
|
||||
import { Card, CardCover, Chip, Modal, ModalDialog, Stack, Typography, useTheme } from '@mui/joy';
|
||||
import ModalClose from '@mui/joy/ModalClose';
|
||||
import { FileLibrary, Manga, MangaConnectorId } from '../../../api/data-contracts.ts';
|
||||
import { Manga } from '../../../api/data-contracts.ts';
|
||||
import { ApiContext } from '../../../contexts/ApiContext.tsx';
|
||||
import { MangaContext } from '../../../contexts/MangaContext.tsx';
|
||||
import { FileLibraryContext } from '../../../contexts/FileLibraryContext.tsx';
|
||||
import MarkdownPreview from '@uiw/react-markdown-preview';
|
||||
import DownloadSection from './DownloadSection.tsx';
|
||||
import { DownloadSection } from './DownloadSection.tsx';
|
||||
import ChaptersSection from './ChaptersSection.tsx';
|
||||
|
||||
export default function MangaDetail(props: MangaDetailProps): ReactNode {
|
||||
const Api = useContext(ApiContext);
|
||||
const Manga = useContext(MangaContext);
|
||||
const Libraries = useContext(FileLibraryContext);
|
||||
const theme = useTheme();
|
||||
|
||||
const [manga, setManga] = useState<Manga | undefined>(props.manga);
|
||||
const [library, setLibrary] = useState<FileLibrary | undefined>();
|
||||
const [downloadFromMap, setDownloadFromMap] = useState<Map<MangaConnectorId, boolean>>(
|
||||
new Map()
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (!props.open) return;
|
||||
if (!props.mangaKey) return;
|
||||
if (props.manga != undefined) return;
|
||||
setLibrary(undefined);
|
||||
Manga.GetManga(props.mangaKey).then(setManga);
|
||||
}, [Api, Manga, props]);
|
||||
|
||||
useEffect(() => {
|
||||
const newMap = new Map();
|
||||
setLibrary(Libraries.find((library) => library.key == manga?.fileLibraryId));
|
||||
manga?.mangaConnectorIds.forEach((id) => {
|
||||
newMap.set(id, id.useForDownload);
|
||||
});
|
||||
setDownloadFromMap(newMap);
|
||||
}, [manga, Libraries]);
|
||||
|
||||
const setDownload = async (): Promise<void> => {
|
||||
if (!manga) return Promise.reject();
|
||||
if (library) {
|
||||
const s = await Api.mangaChangeLibraryCreate(manga.key, library?.key)
|
||||
.then((result) => result.ok)
|
||||
.catch(() => false);
|
||||
if (!s) return Promise.reject();
|
||||
}
|
||||
for (const kv of downloadFromMap) {
|
||||
const s = await Api.mangaSetAsDownloadFromCreate(
|
||||
manga?.key,
|
||||
kv[0].mangaConnectorName,
|
||||
kv[1]
|
||||
)
|
||||
.then((result) => result.ok)
|
||||
.catch(() => false);
|
||||
if (!s) return Promise.reject();
|
||||
}
|
||||
return Promise.resolve();
|
||||
};
|
||||
|
||||
const onLibraryChange = (_: any, value: string | null) => {
|
||||
setLibrary(Libraries.find((library) => library.key == value));
|
||||
};
|
||||
|
||||
return (
|
||||
<Modal
|
||||
open={props.open}
|
||||
@@ -139,11 +99,9 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
|
||||
</Stack>
|
||||
<DownloadSection
|
||||
downloadOpen={props.downloadOpen ?? false}
|
||||
library={library}
|
||||
onLibraryChange={onLibraryChange}
|
||||
downloadFromMap={downloadFromMap}
|
||||
setDownload={setDownload}
|
||||
manga={manga}
|
||||
/>
|
||||
<ChaptersSection manga={manga} />
|
||||
</ModalDialog>
|
||||
</Modal>
|
||||
);
|
||||
|
@@ -30,7 +30,7 @@ export default function ChapterNamingScheme(): ReactNode {
|
||||
<TInput
|
||||
defaultValue={settings?.chapterNamingScheme as string}
|
||||
placeholder={'Scheme'}
|
||||
completionAction={schemeChanged}
|
||||
onSubmit={schemeChanged}
|
||||
actionDelay={5000}
|
||||
/>
|
||||
</SettingsItem>
|
||||
|
@@ -23,7 +23,7 @@ export default function DownloadLanguage(): ReactNode {
|
||||
<TInput
|
||||
defaultValue={settings?.downloadLanguage as string}
|
||||
placeholder={"Language code (f.e. 'en')"}
|
||||
completionAction={languageChanged}
|
||||
onSubmit={languageChanged}
|
||||
/>
|
||||
</SettingsItem>
|
||||
);
|
||||
|
@@ -23,7 +23,7 @@ export default function FlareSolverr(): ReactNode {
|
||||
<TInput
|
||||
placeholder={'FlareSolverr URL'}
|
||||
defaultValue={settings?.flareSolverrUrl as string}
|
||||
completionAction={uriChanged}
|
||||
onSubmit={uriChanged}
|
||||
/>
|
||||
</SettingsItem>
|
||||
);
|
||||
|
@@ -18,7 +18,7 @@ export default function Maintenance() {
|
||||
|
||||
return (
|
||||
<SettingsItem title={'Maintenance'}>
|
||||
<TButton completionAction={cleanUnusedManga}>Cleanup unused Manga</TButton>
|
||||
<TButton onClick={cleanUnusedManga}>Cleanup unused Manga</TButton>
|
||||
</SettingsItem>
|
||||
);
|
||||
}
|
||||
|
@@ -60,7 +60,7 @@ export default function Settings({ setApiUri }: { setApiUri: (uri: string) => vo
|
||||
<TInput
|
||||
placeholder={'http(s)://'}
|
||||
defaultValue={Api.baseUrl}
|
||||
completionAction={apiUriChanged}
|
||||
onSubmit={apiUriChanged}
|
||||
/>
|
||||
</SettingsItem>
|
||||
<ImageCompression />
|
||||
|
@@ -98,7 +98,7 @@ export function Search(props: SearchModalProps): ReactNode {
|
||||
<Typography level={'title-lg'}>Enter a search term or URL</Typography>
|
||||
<TInput
|
||||
placeholder={'Manga-name or URL'}
|
||||
completionAction={startSearch}
|
||||
onSubmit={startSearch}
|
||||
/>
|
||||
</Step>
|
||||
</Stepper>
|
||||
|
Reference in New Issue
Block a user