prettier new config

This commit is contained in:
2025-09-05 00:59:55 +02:00
parent 097e991aec
commit e5186bf72d
33 changed files with 757 additions and 763 deletions

View File

@@ -1,6 +1,11 @@
{ {
"trailingComma": "es5", "trailingComma": "es5",
"tabWidth": 4, "tabWidth": 4,
"semi": false, "singleQuote": true,
"singleQuote": true "printWidth": 80,
"semi": true,
"bracketSpacing": true,
"objectWrap": "collapse",
"bracketSameLine": true,
"singleAttributePerLine": true
} }

View File

@@ -28,15 +28,15 @@ export default tseslint.config({
tsconfigRootDir: import.meta.dirname, tsconfigRootDir: import.meta.dirname,
}, },
}, },
}) });
``` ```
You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules: You can also install [eslint-plugin-react-x](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-x) and [eslint-plugin-react-dom](https://github.com/Rel1cx/eslint-react/tree/main/packages/plugins/eslint-plugin-react-dom) for React-specific lint rules:
```js ```js
// eslint.config.js // eslint.config.js
import reactX from 'eslint-plugin-react-x' import reactX from 'eslint-plugin-react-x';
import reactDom from 'eslint-plugin-react-dom' import reactDom from 'eslint-plugin-react-dom';
export default tseslint.config({ export default tseslint.config({
plugins: { plugins: {
@@ -50,5 +50,5 @@ export default tseslint.config({
...reactX.configs['recommended-typescript'].rules, ...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules, ...reactDom.configs.recommended.rules,
}, },
}) });
``` ```

View File

@@ -1,22 +1,16 @@
import js from '@eslint/js' import js from '@eslint/js';
import globals from 'globals' import globals from 'globals';
import reactHooks from 'eslint-plugin-react-hooks' import reactHooks from 'eslint-plugin-react-hooks';
import reactRefresh from 'eslint-plugin-react-refresh' import reactRefresh from 'eslint-plugin-react-refresh';
import tseslint from 'typescript-eslint' import tseslint from 'typescript-eslint';
export default tseslint.config( export default tseslint.config(
{ ignores: ['dist'] }, { ignores: ['dist'] },
{ {
extends: [js.configs.recommended, ...tseslint.configs.recommended], extends: [js.configs.recommended, ...tseslint.configs.recommended],
files: ['**/*.{ts,tsx}'], files: ['**/*.{ts,tsx}'],
languageOptions: { languageOptions: { ecmaVersion: 2020, globals: globals.browser },
ecmaVersion: 2020, plugins: { 'react-hooks': reactHooks, 'react-refresh': reactRefresh },
globals: globals.browser,
},
plugins: {
'react-hooks': reactHooks,
'react-refresh': reactRefresh,
},
rules: { rules: {
...reactHooks.configs.recommended.rules, ...reactHooks.configs.recommended.rules,
'react-refresh/only-export-components': [ 'react-refresh/only-export-components': [
@@ -25,4 +19,4 @@ export default tseslint.config(
], ],
}, },
} }
) );

View File

@@ -2,12 +2,19 @@
<html lang="en"> <html lang="en">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" type="image/png" href="/blahaj.png" /> <link
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> rel="icon"
type="image/png"
href="/blahaj.png" />
<meta
name="viewport"
content="width=device-width, initial-scale=1.0" />
<title>Tranga</title> <title>Tranga</title>
</head> </head>
<body> <body>
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/main.tsx"></script> <script
type="module"
src="/src/main.tsx"></script>
</body> </body>
</html> </html>

View File

@@ -1,14 +1,14 @@
import Sheet from '@mui/joy/Sheet' import Sheet from '@mui/joy/Sheet';
import Header from './Header.tsx' import Header from './Header.tsx';
import Settings from './Components/Settings/Settings.tsx' import Settings from './Components/Settings/Settings.tsx';
import ApiProvider from './contexts/ApiContext.tsx' import ApiProvider from './contexts/ApiContext.tsx';
import { useEffect, useState } from 'react' import { useEffect, useState } from 'react';
import { ApiConfig } from './api/http-client.ts' import { ApiConfig } from './api/http-client.ts';
import MangaProvider from './contexts/MangaContext.tsx' import MangaProvider from './contexts/MangaContext.tsx';
import MangaList from './Components/Mangas/MangaList.tsx' import MangaList from './Components/Mangas/MangaList.tsx';
import { Search } from './Search.tsx' import { Search } from './Search.tsx';
import MangaConnectorProvider from './contexts/MangaConnectorContext.tsx' import MangaConnectorProvider from './contexts/MangaConnectorContext.tsx';
import LibraryProvider from './contexts/FileLibraryContext.tsx' import LibraryProvider from './contexts/FileLibraryContext.tsx';
export default function App() { export default function App() {
const [apiUri, setApiUri] = useState<string>( const [apiUri, setApiUri] = useState<string>(
@@ -17,13 +17,13 @@ export default function App() {
0, 0,
window.location.href.lastIndexOf('/') window.location.href.lastIndexOf('/')
) + '/api' ) + '/api'
) );
const [apiConfig, setApiConfig] = useState<ApiConfig>({ baseUrl: apiUri }) const [apiConfig, setApiConfig] = useState<ApiConfig>({ baseUrl: apiUri });
useEffect(() => { useEffect(() => {
setApiConfig({ baseUrl: apiUri }) setApiConfig({ baseUrl: apiUri });
}, [apiUri]) }, [apiUri]);
const [searchOpen, setSearchOpen] = useState<boolean>(false) const [searchOpen, setSearchOpen] = useState<boolean>(false);
return ( return (
<ApiProvider apiConfig={apiConfig}> <ApiProvider apiConfig={apiConfig}>
@@ -48,5 +48,5 @@ export default function App() {
</LibraryProvider> </LibraryProvider>
</MangaConnectorProvider> </MangaConnectorProvider>
</ApiProvider> </ApiProvider>
) );
} }

View File

@@ -1,19 +1,19 @@
import { Button } from '@mui/joy' import { Button } from '@mui/joy';
import TProps, { TColor, TDisabled, TState } from './TProps.ts' import TProps, { TColor, TDisabled, TState } from './TProps.ts';
import { MouseEventHandler, ReactNode, useState } from 'react' import { MouseEventHandler, ReactNode, useState } from 'react';
export default function TButton(props: TButtonProps) { export default function TButton(props: TButtonProps) {
const [state, setState] = useState<TState>(TState.clean) const [state, setState] = useState<TState>(TState.clean);
const clicked: MouseEventHandler<HTMLAnchorElement> = (e) => { const clicked: MouseEventHandler<HTMLAnchorElement> = (e) => {
setState(TState.busy) setState(TState.busy);
e.preventDefault() e.preventDefault();
if (props.completionAction) if (props.completionAction)
props props
.completionAction(undefined) .completionAction(undefined)
.then(() => setState(TState.success)) .then(() => setState(TState.success))
.catch(() => setState(TState.failure)) .catch(() => setState(TState.failure));
} };
return ( return (
<Button <Button
@@ -21,13 +21,12 @@ export default function TButton(props: TButtonProps) {
disabled={props.disabled ?? TDisabled(state)} disabled={props.disabled ?? TDisabled(state)}
aria-disabled={props.disabled ?? TDisabled(state)} aria-disabled={props.disabled ?? TDisabled(state)}
onClick={clicked} onClick={clicked}
className={'t-loadable'} className={'t-loadable'}>
>
{props.children} {props.children}
</Button> </Button>
) );
} }
export interface TButtonProps extends TProps { export interface TButtonProps extends TProps {
children?: ReactNode children?: ReactNode;
} }

View File

@@ -1,59 +1,59 @@
import { Button, Input } from '@mui/joy' import { Button, Input } from '@mui/joy';
import { MouseEventHandler, useEffect, useState } from 'react' import { MouseEventHandler, useEffect, useState } from 'react';
import * as React from 'react' import * as React from 'react';
import TProps, { TColor, TDisabled, TState } from './TProps.ts' import TProps, { TColor, TDisabled, TState } from './TProps.ts';
import './loadingBorder.css' import './loadingBorder.css';
export default function TInput(props: TInputProps) { export default function TInput(props: TInputProps) {
const [state, setState] = useState<TState>(TState.clean) const [state, setState] = useState<TState>(TState.clean);
const [value, setValue] = useState< const [value, setValue] = useState<
string | number | readonly string[] | undefined string | number | readonly string[] | undefined
>(props.defaultValue) >(props.defaultValue);
const [initialValue, setInitialValue] = useState< const [initialValue, setInitialValue] = useState<
string | number | readonly string[] | undefined string | number | readonly string[] | undefined
>(props.defaultValue) >(props.defaultValue);
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined) const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const inputValueChanged = (e: React.ChangeEvent<HTMLInputElement>) => { const inputValueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
e.preventDefault() e.preventDefault();
setValue(e.target.value) setValue(e.target.value);
clearTimeout(timerRef.current) clearTimeout(timerRef.current);
if (!props.autoSubmit) return if (!props.autoSubmit) return;
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
submit() submit();
}, props.actionDelay ?? 1500) }, props.actionDelay ?? 1500);
} };
const submitClicked: MouseEventHandler<HTMLAnchorElement> = (e) => { const submitClicked: MouseEventHandler<HTMLAnchorElement> = (e) => {
e.preventDefault() e.preventDefault();
submit() submit();
} };
const keyDownHandler = (e: React.KeyboardEvent<HTMLInputElement>) => { const keyDownHandler = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') submit() if (e.key === 'Enter') submit();
} };
const submit = () => { const submit = () => {
setState(TState.busy) setState(TState.busy);
clearTimeout(timerRef.current) clearTimeout(timerRef.current);
if (props.completionAction) if (props.completionAction)
props props
.completionAction(value) .completionAction(value)
.then(() => { .then(() => {
setState(TState.success) setState(TState.success);
setInitialValue(value) setInitialValue(value);
}) })
.catch(() => setState(TState.failure)) .catch(() => setState(TState.failure));
} };
useEffect(() => { useEffect(() => {
if (value == initialValue) { if (value == initialValue) {
setState(TState.clean) setState(TState.clean);
} }
}, [value, initialValue]) }, [value, initialValue]);
return ( return (
<Input <Input
@@ -71,21 +71,20 @@ export default function TInput(props: TInputProps) {
onClick={submitClicked} onClick={submitClicked}
disabled={props.disabled ?? TDisabled(state)} disabled={props.disabled ?? TDisabled(state)}
aria-disabled={props.disabled ?? TDisabled(state)} aria-disabled={props.disabled ?? TDisabled(state)}
className={'t-loadable'} className={'t-loadable'}>
>
{props.submitButtonText ?? 'Submit'} {props.submitButtonText ?? 'Submit'}
</Button> </Button>
) )
} }
/> />
) );
} }
export interface TInputProps extends TProps { export interface TInputProps extends TProps {
placeholder?: string placeholder?: string;
defaultValue?: string | number | readonly string[] defaultValue?: string | number | readonly string[];
actionDelay?: number actionDelay?: number;
autoSubmit?: boolean autoSubmit?: boolean;
submitButtonHidden?: boolean submitButtonHidden?: boolean;
submitButtonText?: string submitButtonText?: string;
} }

View File

@@ -1,4 +1,4 @@
import { ColorPaletteProp } from '@mui/joy' import { ColorPaletteProp } from '@mui/joy';
export enum TState { export enum TState {
clean, clean,
@@ -11,30 +11,30 @@ export enum TState {
export const TDisabled = (state: TState): boolean => { export const TDisabled = (state: TState): boolean => {
switch (state) { switch (state) {
case TState.busy: case TState.busy:
return true return true;
default: default:
return false return false;
} }
} };
export const TColor = (state: TState): ColorPaletteProp => { export const TColor = (state: TState): ColorPaletteProp => {
switch (state) { switch (state) {
case TState.clean: case TState.clean:
return 'primary' return 'primary';
case TState.dirty: case TState.dirty:
return 'warning' return 'warning';
case TState.busy: case TState.busy:
return 'neutral' return 'neutral';
case TState.success: case TState.success:
return 'success' return 'success';
case TState.failure: case TState.failure:
return 'warning' return 'warning';
} }
} };
export default interface TProps { export default interface TProps {
disabled?: boolean disabled?: boolean;
completionAction?: ( completionAction?: (
value?: string | number | readonly string[] value?: string | number | readonly string[]
) => Promise<void> ) => Promise<void>;
} }

View File

@@ -6,19 +6,19 @@ import {
ColorPaletteProp, ColorPaletteProp,
Skeleton, Skeleton,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy';
import { EventHandler, ReactNode, useContext } from 'react' import { EventHandler, ReactNode, useContext } from 'react';
import './MangaCard.css' import './MangaCard.css';
import MangaConnectorIcon from './MangaConnectorIcon.tsx' import MangaConnectorIcon from './MangaConnectorIcon.tsx';
import { import {
Manga, Manga,
MangaReleaseStatus, MangaReleaseStatus,
MinimalManga, MinimalManga,
} from '../../api/data-contracts.ts' } from '../../api/data-contracts.ts';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
export default function MangaCard(props: MangaCardProps): ReactNode { export default function MangaCard(props: MangaCardProps): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
return ( return (
<Badge <Badge
@@ -30,9 +30,10 @@ export default function MangaCard(props: MangaCardProps): ReactNode {
className={'manga-card-badge'} className={'manga-card-badge'}
color={releaseColor( color={releaseColor(
props.manga?.releaseStatus ?? MangaReleaseStatus.Unreleased props.manga?.releaseStatus ?? MangaReleaseStatus.Unreleased
)} )}>
> <Card
<Card className={'manga-card'} onClick={props.onClick}> className={'manga-card'}
onClick={props.onClick}>
<CardCover className={'manga-card-cover'}> <CardCover className={'manga-card-cover'}>
<img <img
src={ src={
@@ -52,31 +53,31 @@ export default function MangaCard(props: MangaCardProps): ReactNode {
</CardContent> </CardContent>
</Card> </Card>
</Badge> </Badge>
) );
} }
export interface MangaCardProps { export interface MangaCardProps {
manga?: Manga | MinimalManga manga?: Manga | MinimalManga;
onClick?: EventHandler<any> onClick?: EventHandler<any>;
} }
const stringWithRandomLength = (): string => { const stringWithRandomLength = (): string => {
return 'wow' return 'wow';
} };
const releaseColor = (status: MangaReleaseStatus): ColorPaletteProp => { const releaseColor = (status: MangaReleaseStatus): ColorPaletteProp => {
switch (status) { switch (status) {
case MangaReleaseStatus.Cancelled: case MangaReleaseStatus.Cancelled:
return 'danger' return 'danger';
case MangaReleaseStatus.Completed: case MangaReleaseStatus.Completed:
return 'success' return 'success';
case MangaReleaseStatus.Unreleased: case MangaReleaseStatus.Unreleased:
return 'neutral' return 'neutral';
case MangaReleaseStatus.Continuing: case MangaReleaseStatus.Continuing:
return 'primary' return 'primary';
case MangaReleaseStatus.OnHiatus: case MangaReleaseStatus.OnHiatus:
return 'neutral' return 'neutral';
default: default:
return 'neutral' return 'neutral';
} }
} };

View File

@@ -1,33 +1,33 @@
import { ReactNode, useContext, useEffect, useState } from 'react' import { ReactNode, useContext, useEffect, useState } from 'react';
import { MangaConnector } from '../../api/data-contracts.ts' import { MangaConnector } from '../../api/data-contracts.ts';
import { Tooltip } from '@mui/joy' import { Tooltip } from '@mui/joy';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
export default function MangaConnectorIcon({ export default function MangaConnectorIcon({
mangaConnector, mangaConnector,
mangaConnectorName, mangaConnectorName,
}: { }: {
mangaConnector?: MangaConnector mangaConnector?: MangaConnector;
mangaConnectorName?: string mangaConnectorName?: string;
}): ReactNode { }): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const [connector, setConnector] = useState<MangaConnector | undefined>( const [connector, setConnector] = useState<MangaConnector | undefined>(
mangaConnector mangaConnector
) );
useEffect(() => { useEffect(() => {
if (mangaConnector) { if (mangaConnector) {
setConnector(mangaConnector) setConnector(mangaConnector);
return return;
} }
if (!mangaConnectorName) return if (!mangaConnectorName) return;
Api.mangaConnectorDetail(mangaConnectorName).then((result) => { Api.mangaConnectorDetail(mangaConnectorName).then((result) => {
if (result.ok) { if (result.ok) {
setConnector(result.data) setConnector(result.data);
} }
}) });
}, [Api, mangaConnectorName, mangaConnector]) }, [Api, mangaConnectorName, mangaConnector]);
return ( return (
<Tooltip title={connector?.name ?? 'loading'}> <Tooltip title={connector?.name ?? 'loading'}>
@@ -38,5 +38,5 @@ export default function MangaConnectorIcon({
style={{ borderRadius: '100%' }} style={{ borderRadius: '100%' }}
/> />
</Tooltip> </Tooltip>
) );
} }

View File

@@ -1,29 +1,31 @@
import { Stack } from '@mui/joy' import { Stack } from '@mui/joy';
import './MangaList.css' import './MangaList.css';
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react' import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react';
import { import {
Manga, Manga,
MangaReleaseStatus, MangaReleaseStatus,
MinimalManga, MinimalManga,
} from '../../api/data-contracts.ts' } from '../../api/data-contracts.ts';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
import MangaCard from './MangaCard.tsx' import MangaCard from './MangaCard.tsx';
export default function MangaList({ export default function MangaList({
openSearch, openSearch,
}: { }: {
openSearch: () => void openSearch: () => void;
}): ReactNode { }): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const [downloadingManga, setDownloadingManga] = useState<MinimalManga[]>([]) const [downloadingManga, setDownloadingManga] = useState<MinimalManga[]>(
[]
);
useEffect(() => { useEffect(() => {
Api.mangaDownloadingList().then((data) => { Api.mangaDownloadingList().then((data) => {
if (data.ok) { if (data.ok) {
setDownloadingManga(data.data) setDownloadingManga(data.data);
} }
}) });
}, [Api]) }, [Api]);
return ( return (
<MangaCardList manga={downloadingManga}> <MangaCardList manga={downloadingManga}>
@@ -38,7 +40,7 @@ export default function MangaList({
}} }}
/> />
</MangaCardList> </MangaCardList>
) );
} }
export function MangaCardList(props: MangaCardListProps): ReactNode { export function MangaCardList(props: MangaCardListProps): ReactNode {
@@ -52,24 +54,23 @@ export function MangaCardList(props: MangaCardListProps): ReactNode {
px: 'var(--ModalDialog-padding)', px: 'var(--ModalDialog-padding)',
overflowY: 'scroll', overflowY: 'scroll',
justifyItems: 'space-between', justifyItems: 'space-between',
}} }}>
>
{props.children} {props.children}
{props.manga.map((m) => ( {props.manga.map((m) => (
<MangaCard <MangaCard
key={m.key} key={m.key}
manga={m} manga={m}
onClick={() => { onClick={() => {
if (props.mangaOnClick) props.mangaOnClick(m) if (props.mangaOnClick) props.mangaOnClick(m);
}} }}
/> />
))} ))}
</Stack> </Stack>
) );
} }
export interface MangaCardListProps { export interface MangaCardListProps {
manga: (Manga | MinimalManga)[] manga: (Manga | MinimalManga)[];
children?: ReactNode children?: ReactNode;
mangaOnClick?: Dispatch<Manga | MinimalManga> mangaOnClick?: Dispatch<Manga | MinimalManga>;
} }

View File

@@ -1,26 +1,26 @@
import { ReactNode, useContext } from 'react' import { ReactNode, useContext } from 'react';
import { SettingsContext, SettingsItem } from './Settings.tsx' import { SettingsContext, SettingsItem } from './Settings.tsx';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
import MarkdownPreview from '@uiw/react-markdown-preview' import MarkdownPreview from '@uiw/react-markdown-preview';
import TInput from '../Inputs/TInput.tsx' import TInput from '../Inputs/TInput.tsx';
export default function ChapterNamingScheme(): ReactNode { export default function ChapterNamingScheme(): ReactNode {
const settings = useContext(SettingsContext) const settings = useContext(SettingsContext);
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const schemeChanged = async ( const schemeChanged = async (
value: string | number | readonly string[] | undefined value: string | number | readonly string[] | undefined
) => { ) => {
if (typeof value != 'string') return Promise.reject() if (typeof value != 'string') return Promise.reject();
try { try {
const response = const response =
await Api.settingsChapterNamingSchemePartialUpdate(value) await Api.settingsChapterNamingSchemePartialUpdate(value);
if (response.ok) return Promise.resolve() if (response.ok) return Promise.resolve();
else return Promise.reject() else return Promise.reject();
} catch { } catch {
return await Promise.reject() return await Promise.reject();
} }
} };
return ( return (
<SettingsItem title={'Chapter Naming Scheme'}> <SettingsItem title={'Chapter Naming Scheme'}>
@@ -37,5 +37,5 @@ export default function ChapterNamingScheme(): ReactNode {
actionDelay={5000} actionDelay={5000}
/> />
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,7 +1,7 @@
import { SettingsItem } from './Settings.tsx' import { SettingsItem } from './Settings.tsx';
import ImageCompression from './ImageCompression.tsx' import ImageCompression from './ImageCompression.tsx';
import DownloadLanguage from './DownloadLanguage.tsx' import DownloadLanguage from './DownloadLanguage.tsx';
import ChapterNamingScheme from './ChapterNamingScheme.tsx' import ChapterNamingScheme from './ChapterNamingScheme.tsx';
export default function Download() { export default function Download() {
return ( return (
@@ -10,5 +10,5 @@ export default function Download() {
<DownloadLanguage /> <DownloadLanguage />
<ChapterNamingScheme /> <ChapterNamingScheme />
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,25 +1,25 @@
import { ReactNode, useContext } from 'react' import { ReactNode, useContext } from 'react';
import { SettingsContext, SettingsItem } from './Settings.tsx' import { SettingsContext, SettingsItem } from './Settings.tsx';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
import TInput from '../Inputs/TInput.tsx' import TInput from '../Inputs/TInput.tsx';
export default function DownloadLanguage(): ReactNode { export default function DownloadLanguage(): ReactNode {
const settings = useContext(SettingsContext) const settings = useContext(SettingsContext);
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const languageChanged = async ( const languageChanged = async (
value: string | number | readonly string[] | undefined value: string | number | readonly string[] | undefined
) => { ) => {
if (typeof value != 'string') return Promise.reject() if (typeof value != 'string') return Promise.reject();
try { try {
const response = const response =
await Api.settingsDownloadLanguagePartialUpdate(value) await Api.settingsDownloadLanguagePartialUpdate(value);
if (response.ok) return Promise.resolve() if (response.ok) return Promise.resolve();
else return Promise.reject() else return Promise.reject();
} catch { } catch {
return await Promise.reject() return await Promise.reject();
} }
} };
return ( return (
<SettingsItem title={'Download Language'}> <SettingsItem title={'Download Language'}>
@@ -29,5 +29,5 @@ export default function DownloadLanguage(): ReactNode {
completionAction={languageChanged} completionAction={languageChanged}
/> />
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,24 +1,24 @@
import { ReactNode, useContext } from 'react' import { ReactNode, useContext } from 'react';
import { SettingsContext, SettingsItem } from './Settings.tsx' import { SettingsContext, SettingsItem } from './Settings.tsx';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
import TInput from '../Inputs/TInput.tsx' import TInput from '../Inputs/TInput.tsx';
export default function FlareSolverr(): ReactNode { export default function FlareSolverr(): ReactNode {
const settings = useContext(SettingsContext) const settings = useContext(SettingsContext);
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const uriChanged = async ( const uriChanged = async (
value: string | number | readonly string[] | undefined value: string | number | readonly string[] | undefined
) => { ) => {
if (typeof value != 'string') return Promise.reject() if (typeof value != 'string') return Promise.reject();
try { try {
const response = await Api.settingsFlareSolverrUrlCreate(value) const response = await Api.settingsFlareSolverrUrlCreate(value);
if (response.ok) return Promise.resolve() if (response.ok) return Promise.resolve();
else return Promise.reject() else return Promise.reject();
} catch (reason) { } catch (reason) {
return await Promise.reject(reason) return await Promise.reject(reason);
} }
} };
return ( return (
<SettingsItem title={'FlareSolverr'}> <SettingsItem title={'FlareSolverr'}>
@@ -28,5 +28,5 @@ export default function FlareSolverr(): ReactNode {
completionAction={uriChanged} completionAction={uriChanged}
/> />
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,17 +1,16 @@
import { ReactNode, useContext } from 'react' import { ReactNode, useContext } from 'react';
import { SettingsContext, SettingsItem } from './Settings.tsx' import { SettingsContext, SettingsItem } from './Settings.tsx';
import { Slider } from '@mui/joy' import { Slider } from '@mui/joy';
export default function ImageCompression(): ReactNode { export default function ImageCompression(): ReactNode {
const settings = useContext(SettingsContext) const settings = useContext(SettingsContext);
return ( return (
<SettingsItem title={'Image Compression'}> <SettingsItem title={'Image Compression'}>
<Slider <Slider
sx={{ marginTop: '20px' }} sx={{ marginTop: '20px' }}
valueLabelDisplay={'auto'} valueLabelDisplay={'auto'}
defaultValue={settings?.imageCompression} defaultValue={settings?.imageCompression}></Slider>
></Slider>
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,20 +1,20 @@
import { SettingsItem } from './Settings.tsx' import { SettingsItem } from './Settings.tsx';
import { useContext } from 'react' import { useContext } from 'react';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
import TButton from '../Inputs/TButton.tsx' import TButton from '../Inputs/TButton.tsx';
export default function Maintenance() { export default function Maintenance() {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const cleanUnusedManga = async (): Promise<void> => { const cleanUnusedManga = async (): Promise<void> => {
try { try {
const result = await Api.maintenanceCleanupNoDownloadMangaCreate() const result = await Api.maintenanceCleanupNoDownloadMangaCreate();
if (result.ok) return Promise.resolve() if (result.ok) return Promise.resolve();
else return Promise.reject() else return Promise.reject();
} catch (reason) { } catch (reason) {
return await Promise.reject(reason) return await Promise.reject(reason);
} }
} };
return ( return (
<SettingsItem title={'Maintenance'}> <SettingsItem title={'Maintenance'}>
@@ -22,5 +22,5 @@ export default function Maintenance() {
Cleanup unused Manga Cleanup unused Manga
</TButton> </TButton>
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,11 +1,11 @@
import { SettingsItem } from './Settings.tsx' import { SettingsItem } from './Settings.tsx';
import FlareSolverr from './FlareSolverr.tsx' import FlareSolverr from './FlareSolverr.tsx';
import { ReactNode } from 'react' import { ReactNode } from 'react';
export default function Services(): ReactNode { export default function Services(): ReactNode {
return ( return (
<SettingsItem title={'Services'}> <SettingsItem title={'Services'}>
<FlareSolverr /> <FlareSolverr />
</SettingsItem> </SettingsItem>
) );
} }

View File

@@ -1,4 +1,4 @@
import ModalClose from '@mui/joy/ModalClose' import ModalClose from '@mui/joy/ModalClose';
import { import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
@@ -9,63 +9,62 @@ import {
DialogTitle, DialogTitle,
Modal, Modal,
ModalDialog, ModalDialog,
} from '@mui/joy' } from '@mui/joy';
import './Settings.css' import './Settings.css';
import * as React from 'react' import * as React from 'react';
import { import {
createContext, createContext,
ReactNode, ReactNode,
useContext, useContext,
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react';
import { SxProps } from '@mui/joy/styles/types' import { SxProps } from '@mui/joy/styles/types';
import ImageCompression from './ImageCompression.tsx' import ImageCompression from './ImageCompression.tsx';
import FlareSolverr from './FlareSolverr.tsx' import FlareSolverr from './FlareSolverr.tsx';
import DownloadLanguage from './DownloadLanguage.tsx' import DownloadLanguage from './DownloadLanguage.tsx';
import ChapterNamingScheme from './ChapterNamingScheme.tsx' import ChapterNamingScheme from './ChapterNamingScheme.tsx';
import Maintenance from './Maintenance.tsx' import Maintenance from './Maintenance.tsx';
import { ApiContext } from '../../contexts/ApiContext.tsx' import { ApiContext } from '../../contexts/ApiContext.tsx';
import { TrangaSettings } from '../../api/data-contracts.ts' import { TrangaSettings } from '../../api/data-contracts.ts';
import TInput from '../Inputs/TInput.tsx' import TInput from '../Inputs/TInput.tsx';
export const SettingsContext = createContext<TrangaSettings | undefined>( export const SettingsContext = createContext<TrangaSettings | undefined>(
undefined undefined
) );
export default function Settings({ export default function Settings({
setApiUri, setApiUri,
}: { }: {
setApiUri: (uri: string) => void setApiUri: (uri: string) => void;
}) { }) {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const [settings, setSettings] = useState<TrangaSettings>() const [settings, setSettings] = useState<TrangaSettings>();
const [open, setOpen] = React.useState(false) const [open, setOpen] = React.useState(false);
useEffect(() => { useEffect(() => {
Api.settingsList().then((response) => { Api.settingsList().then((response) => {
setSettings(response.data) setSettings(response.data);
}) });
}, [Api]) }, [Api]);
const apiUriChanged = ( const apiUriChanged = (
value: string | number | readonly string[] | undefined value: string | number | readonly string[] | undefined
) => { ) => {
if (typeof value != 'string') return Promise.reject() if (typeof value != 'string') return Promise.reject();
setApiUri(value) setApiUri(value);
return Promise.resolve() return Promise.resolve();
} };
const ModalStyle: SxProps = { const ModalStyle: SxProps = { width: '80%', height: '80%' };
width: '80%',
height: '80%',
}
return ( return (
<SettingsContext.Provider value={settings}> <SettingsContext.Provider value={settings}>
<Button onClick={() => setOpen(true)}>Settings</Button> <Button onClick={() => setOpen(true)}>Settings</Button>
<Modal open={open} onClose={() => setOpen(false)}> <Modal
open={open}
onClose={() => setOpen(false)}>
<ModalDialog sx={ModalStyle}> <ModalDialog sx={ModalStyle}>
<ModalClose /> <ModalClose />
<DialogTitle>Settings</DialogTitle> <DialogTitle>Settings</DialogTitle>
@@ -88,20 +87,20 @@ export default function Settings({
</ModalDialog> </ModalDialog>
</Modal> </Modal>
</SettingsContext.Provider> </SettingsContext.Provider>
) );
} }
export function SettingsItem({ export function SettingsItem({
title, title,
children, children,
}: { }: {
title: string title: string;
children: ReactNode children: ReactNode;
}) { }) {
return ( return (
<Accordion> <Accordion>
<AccordionSummary>{title}</AccordionSummary> <AccordionSummary>{title}</AccordionSummary>
<AccordionDetails>{children}</AccordionDetails> <AccordionDetails>{children}</AccordionDetails>
</Accordion> </Accordion>
) );
} }

View File

@@ -1,16 +1,16 @@
import Sheet from '@mui/joy/Sheet' import Sheet from '@mui/joy/Sheet';
import { Link, Stack, Typography } from '@mui/joy' import { Link, Stack, Typography } from '@mui/joy';
import { ReactElement, ReactNode, useContext } from 'react' import { ReactElement, ReactNode, useContext } from 'react';
import './Header.css' import './Header.css';
import { Article, GitHub } from '@mui/icons-material' import { Article, GitHub } from '@mui/icons-material';
import { ApiContext } from './contexts/ApiContext.tsx' import { ApiContext } from './contexts/ApiContext.tsx';
export default function Header({ export default function Header({
children, children,
}: { }: {
children?: ReactNode children?: ReactNode;
}): ReactElement { }): ReactElement {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
return ( return (
<Sheet className={'header'}> <Sheet className={'header'}>
@@ -22,13 +22,11 @@ export default function Header({
alignItems: 'center', alignItems: 'center',
justifyContent: 'space-between', justifyContent: 'space-between',
}} }}
useFlexGap useFlexGap>
>
<Stack <Stack
sx={{ flexGrow: 1, flexBasis: 1 }} sx={{ flexGrow: 1, flexBasis: 1 }}
direction={'row'} direction={'row'}
spacing={2} spacing={2}>
>
{children} {children}
</Stack> </Stack>
<Stack <Stack
@@ -38,8 +36,7 @@ export default function Header({
flexBasis: 1, flexBasis: 1,
justifyContent: 'center', justifyContent: 'center',
}} }}
direction={'row'} direction={'row'}>
>
<img <img
src={'/blahaj.png'} src={'/blahaj.png'}
style={{ cursor: 'grab', maxHeight: '100%' }} style={{ cursor: 'grab', maxHeight: '100%' }}
@@ -53,8 +50,7 @@ export default function Header({
WebkitTextFillColor: 'transparent', WebkitTextFillColor: 'transparent',
fontWeight: 'bold', fontWeight: 'bold',
cursor: 'default', cursor: 'default',
}} }}>
>
Tranga Tranga
</Typography> </Typography>
</Stack> </Stack>
@@ -65,35 +61,31 @@ export default function Header({
justifyContent: 'flex-end', justifyContent: 'flex-end',
}} }}
direction={'row'} direction={'row'}
spacing={2} spacing={2}>
>
<Link <Link
target={'_blank'} target={'_blank'}
href={'https://github.com/C9Glax/tranga'} href={'https://github.com/C9Glax/tranga'}
color={'neutral'} color={'neutral'}
height={'min-content'} height={'min-content'}>
>
<GitHub /> Server <GitHub /> Server
</Link> </Link>
<Link <Link
target={'_blank'} target={'_blank'}
href={'https://github.com/C9Glax/tranga-website'} href={'https://github.com/C9Glax/tranga-website'}
color={'neutral'} color={'neutral'}
height={'min-content'} height={'min-content'}>
>
<GitHub /> Website <GitHub /> Website
</Link> </Link>
<Link <Link
target={'_blank'} target={'_blank'}
href={Api.baseUrl + '/swagger'} href={Api.baseUrl + '/swagger'}
color={'neutral'} color={'neutral'}
height={'min-content'} height={'min-content'}>
>
<Article /> <Article />
Swagger Swagger
</Link> </Link>
</Stack> </Stack>
</Stack> </Stack>
</Sheet> </Sheet>
) );
} }

View File

@@ -1,5 +1,5 @@
import { Manga } from './api/data-contracts.ts' import { Manga } from './api/data-contracts.ts';
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react' import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react';
import { import {
Card, Card,
CardCover, CardCover,
@@ -9,30 +9,32 @@ import {
Stack, Stack,
Typography, Typography,
useTheme, useTheme,
} from '@mui/joy' } from '@mui/joy';
import ModalClose from '@mui/joy/ModalClose' import ModalClose from '@mui/joy/ModalClose';
import { ApiContext } from './contexts/ApiContext.tsx' import { ApiContext } from './contexts/ApiContext.tsx';
import { MangaContext } from './contexts/MangaContext.tsx' import { MangaContext } from './contexts/MangaContext.tsx';
import './Components/Mangas/MangaCard.css' import './Components/Mangas/MangaCard.css';
import MarkdownPreview from '@uiw/react-markdown-preview' import MarkdownPreview from '@uiw/react-markdown-preview';
export default function MangaDetail(props: MangaDetailProps): ReactNode { export default function MangaDetail(props: MangaDetailProps): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const Manga = useContext(MangaContext) const Manga = useContext(MangaContext);
const [manga, setManga] = useState<Manga | undefined>(props.manga) const [manga, setManga] = useState<Manga | undefined>(props.manga);
useEffect(() => { useEffect(() => {
if (!props.open) return if (!props.open) return;
if (!props.mangaKey) return if (!props.mangaKey) return;
if (props.manga != undefined) return if (props.manga != undefined) return;
Manga.GetManga(props.mangaKey).then(setManga) Manga.GetManga(props.mangaKey).then(setManga);
}, [Api, Manga, props]) }, [Api, Manga, props]);
const theme = useTheme() const theme = useTheme();
return ( return (
<Modal open={props.open} onClose={() => props.setOpen(false)}> <Modal
open={props.open}
onClose={() => props.setOpen(false)}>
<ModalDialog> <ModalDialog>
<ModalClose /> <ModalClose />
<div <div
@@ -40,9 +42,10 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
display: 'flex', display: 'flex',
flexWrap: 'wrap', flexWrap: 'wrap',
flexDirection: 'row', flexDirection: 'row',
}} }}>
> <Typography
<Typography level={'h3'} sx={{ width: '100%' }}> level={'h3'}
sx={{ width: '100%' }}>
{manga?.name} {manga?.name}
</Typography> </Typography>
<Card className={'manga-card'}> <Card className={'manga-card'}>
@@ -59,9 +62,11 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
<Stack <Stack
direction={'column'} direction={'column'}
gap={2} gap={2}
sx={{ maxWidth: 'calc(100% - 230px)', margin: '5px' }} sx={{ maxWidth: 'calc(100% - 230px)', margin: '5px' }}>
> <Stack
<Stack direction={'row'} gap={0.5} flexWrap={'wrap'}> direction={'row'}
gap={0.5}
flexWrap={'wrap'}>
{manga?.tags.map((tag) => ( {manga?.tags.map((tag) => (
<Chip <Chip
key={tag} key={tag}
@@ -69,8 +74,7 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
sx={{ sx={{
backgroundColor: backgroundColor:
theme.palette.primary.plainColor, theme.palette.primary.plainColor,
}} }}>
>
{tag} {tag}
</Chip> </Chip>
))} ))}
@@ -81,8 +85,7 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
sx={{ sx={{
backgroundColor: backgroundColor:
theme.palette.success.plainColor, theme.palette.success.plainColor,
}} }}>
>
{author.name} {author.name}
</Chip> </Chip>
))} ))}
@@ -93,8 +96,7 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
sx={{ sx={{
backgroundColor: backgroundColor:
theme.palette.neutral.plainColor, theme.palette.neutral.plainColor,
}} }}>
>
<a href={link.url}>{link.provider}</a> <a href={link.url}>{link.provider}</a>
</Chip> </Chip>
))} ))}
@@ -116,20 +118,19 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
alignItems: 'flex-end', alignItems: 'flex-end',
}} }}
flexWrap={'nowrap'} flexWrap={'nowrap'}
gap={1} gap={1}>
>
{props.actions} {props.actions}
</Stack> </Stack>
</div> </div>
</ModalDialog> </ModalDialog>
</Modal> </Modal>
) );
} }
export interface MangaDetailProps { export interface MangaDetailProps {
manga?: Manga manga?: Manga;
mangaKey?: string mangaKey?: string;
open: boolean open: boolean;
setOpen: Dispatch<boolean> setOpen: Dispatch<boolean>;
actions?: ReactNode[] actions?: ReactNode[];
} }

View File

@@ -1,4 +1,4 @@
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react' import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react';
import { import {
Box, Box,
Card, Card,
@@ -10,56 +10,56 @@ import {
Select, Select,
Stack, Stack,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy';
import ModalClose from '@mui/joy/ModalClose' import ModalClose from '@mui/joy/ModalClose';
import { FileLibrary, Manga, MangaConnectorId } from './api/data-contracts.ts' import { FileLibrary, Manga, MangaConnectorId } from './api/data-contracts.ts';
import { ApiContext } from './contexts/ApiContext.tsx' import { ApiContext } from './contexts/ApiContext.tsx';
import { MangaContext } from './contexts/MangaContext.tsx' import { MangaContext } from './contexts/MangaContext.tsx';
import { FileLibraryContext } from './contexts/FileLibraryContext.tsx' import { FileLibraryContext } from './contexts/FileLibraryContext.tsx';
import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx' import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx';
import TButton from './Components/Inputs/TButton.tsx' import TButton from './Components/Inputs/TButton.tsx';
export default function MangaDownloadDrawer( export default function MangaDownloadDrawer(
props: MangaDownloadDrawerProps props: MangaDownloadDrawerProps
): ReactNode { ): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const Manga = useContext(MangaContext) const Manga = useContext(MangaContext);
const Libraries = useContext(FileLibraryContext) const Libraries = useContext(FileLibraryContext);
const [manga, setManga] = useState<Manga | undefined>(props.manga) const [manga, setManga] = useState<Manga | undefined>(props.manga);
const [library, setLibrary] = useState<FileLibrary | undefined>() const [library, setLibrary] = useState<FileLibrary | undefined>();
const [downloadFromMap, setDownloadFromMap] = useState< const [downloadFromMap, setDownloadFromMap] = useState<
Map<MangaConnectorId, boolean> Map<MangaConnectorId, boolean>
>(new Map()) >(new Map());
useEffect(() => { useEffect(() => {
if (!props.open) return if (!props.open) return;
if (!props.mangaKey) return if (!props.mangaKey) return;
if (props.manga != undefined) return if (props.manga != undefined) return;
Manga.GetManga(props.mangaKey).then(setManga) Manga.GetManga(props.mangaKey).then(setManga);
}, [Api, Manga, props]) }, [Api, Manga, props]);
useEffect(() => { useEffect(() => {
const newMap = new Map() const newMap = new Map();
setLibrary( setLibrary(
Libraries.find((library) => library.key == manga?.fileLibraryId) Libraries.find((library) => library.key == manga?.fileLibraryId)
) );
manga?.mangaConnectorIds.forEach((id) => { manga?.mangaConnectorIds.forEach((id) => {
newMap.set(id, id.useForDownload) newMap.set(id, id.useForDownload);
}) });
setDownloadFromMap(newMap) setDownloadFromMap(newMap);
}, [manga]) }, [manga]);
const setDownload = async (): Promise<void> => { const setDownload = async (): Promise<void> => {
if (!manga) return Promise.reject() if (!manga) return Promise.reject();
if (library) { if (library) {
const s = await Api.mangaChangeLibraryCreate( const s = await Api.mangaChangeLibraryCreate(
manga.key, manga.key,
library?.key library?.key
) )
.then((result) => result.ok) .then((result) => result.ok)
.catch(() => false) .catch(() => false);
if (!s) return Promise.reject() if (!s) return Promise.reject();
} }
for (const kv of downloadFromMap) { for (const kv of downloadFromMap) {
const s = await Api.mangaSetAsDownloadFromCreate( const s = await Api.mangaSetAsDownloadFromCreate(
@@ -68,28 +68,30 @@ export default function MangaDownloadDrawer(
kv[1] kv[1]
) )
.then((result) => result.ok) .then((result) => result.ok)
.catch(() => false) .catch(() => false);
if (!s) return Promise.reject() if (!s) return Promise.reject();
} }
return Promise.resolve() return Promise.resolve();
} };
const onLibraryChange = (_: any, value: string | null) => { const onLibraryChange = (_: any, value: string | null) => {
setLibrary(Libraries.find((library) => library.key == value)) setLibrary(Libraries.find((library) => library.key == value));
} };
return ( return (
<Drawer <Drawer
open={props.open} open={props.open}
onClose={() => props.setOpen(false)} onClose={() => props.setOpen(false)}
anchor="left" anchor="left"
size="md" size="md">
>
<Card sx={{ flexGrow: 1, margin: '10px' }}> <Card sx={{ flexGrow: 1, margin: '10px' }}>
<ModalClose /> <ModalClose />
<Typography level={'h3'}>Download</Typography> <Typography level={'h3'}>Download</Typography>
<Typography level={'h4'}>{manga?.name}</Typography> <Typography level={'h4'}>{manga?.name}</Typography>
<Stack direction={'column'} gap={2} sx={{ flexBasis: 0 }}> <Stack
direction={'column'}
gap={2}
sx={{ flexBasis: 0 }}>
<Box> <Box>
<Typography> <Typography>
Select a Library to Download to: Select a Library to Download to:
@@ -97,10 +99,11 @@ export default function MangaDownloadDrawer(
<Select <Select
placeholder={'Select a Library'} placeholder={'Select a Library'}
value={library?.key} value={library?.key}
onChange={onLibraryChange} onChange={onLibraryChange}>
>
{Libraries.map((l) => ( {Libraries.map((l) => (
<Option key={l.key} value={l.key}> <Option
key={l.key}
value={l.key}>
{l.libraryName} ({l.basePath}) {l.libraryName} ({l.basePath})
</Option> </Option>
))} ))}
@@ -128,8 +131,7 @@ export default function MangaDownloadDrawer(
display: 'flex', display: 'flex',
alignItems: 'center', alignItems: 'center',
gap: 5, gap: 5,
}} }}>
>
<MangaConnectorIcon <MangaConnectorIcon
mangaConnectorName={ mangaConnectorName={
id.mangaConnectorName id.mangaConnectorName
@@ -151,12 +153,12 @@ export default function MangaDownloadDrawer(
</Stack> </Stack>
</Card> </Card>
</Drawer> </Drawer>
) );
} }
export interface MangaDownloadDrawerProps { export interface MangaDownloadDrawerProps {
manga?: Manga manga?: Manga;
mangaKey?: string mangaKey?: string;
open: boolean open: boolean;
setOpen: Dispatch<boolean> setOpen: Dispatch<boolean>;
} }

View File

@@ -1,4 +1,4 @@
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react' import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react';
import { import {
Button, Button,
List, List,
@@ -10,89 +10,91 @@ import {
StepIndicator, StepIndicator,
Stepper, Stepper,
Typography, Typography,
} from '@mui/joy' } from '@mui/joy';
import ModalClose from '@mui/joy/ModalClose' import ModalClose from '@mui/joy/ModalClose';
import { MangaConnectorContext } from './contexts/MangaConnectorContext.tsx' import { MangaConnectorContext } from './contexts/MangaConnectorContext.tsx';
import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx' import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx';
import TInput from './Components/Inputs/TInput.tsx' import TInput from './Components/Inputs/TInput.tsx';
import { ApiContext } from './contexts/ApiContext.tsx' import { ApiContext } from './contexts/ApiContext.tsx';
import { MangaCardList } from './Components/Mangas/MangaList.tsx' import { MangaCardList } from './Components/Mangas/MangaList.tsx';
import { MangaConnector, MinimalManga } from './api/data-contracts.ts' import { MangaConnector, MinimalManga } from './api/data-contracts.ts';
import MangaDetail from './MangaDetail.tsx' import MangaDetail from './MangaDetail.tsx';
import MangaDownloadDrawer from './MangaDownloadDrawer.tsx' import MangaDownloadDrawer from './MangaDownloadDrawer.tsx';
export function Search(props: SearchModalProps): ReactNode { export function Search(props: SearchModalProps): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const MangaConnectors = useContext(MangaConnectorContext) const MangaConnectors = useContext(MangaConnectorContext);
useEffect(() => { useEffect(() => {
if (props.open) { if (props.open) {
setSelectedConnector(undefined) setSelectedConnector(undefined);
setSearchResults([]) setSearchResults([]);
} }
}, [props]) }, [props]);
const [selectedConnector, setSelectedConnector] = useState<MangaConnector>() const [selectedConnector, setSelectedConnector] =
const [searchResults, setSearchResults] = useState<MinimalManga[]>([]) useState<MangaConnector>();
const [searchResults, setSearchResults] = useState<MinimalManga[]>([]);
const startSearch = async ( const startSearch = async (
value: string | number | readonly string[] | undefined value: string | number | readonly string[] | undefined
): Promise<void> => { ): Promise<void> => {
if (typeof value != 'string') return Promise.reject() if (typeof value != 'string') return Promise.reject();
setSearchResults([]) setSearchResults([]);
if (isUrl(value)) { if (isUrl(value)) {
try { try {
const result = await Api.searchUrlCreate(value) const result = await Api.searchUrlCreate(value);
if (result.ok) { if (result.ok) {
setSearchResults([result.data]) setSearchResults([result.data]);
return Promise.resolve() return Promise.resolve();
} else return Promise.reject() } else return Promise.reject();
} catch (reason) { } catch (reason) {
return await Promise.reject(reason) return await Promise.reject(reason);
} }
} else { } else {
if (!selectedConnector) return Promise.reject() if (!selectedConnector) return Promise.reject();
try { try {
const result2 = await Api.searchDetail( const result2 = await Api.searchDetail(
selectedConnector?.key, selectedConnector?.key,
value value
) );
if (result2.ok) { if (result2.ok) {
setSearchResults(result2.data) setSearchResults(result2.data);
return Promise.resolve() return Promise.resolve();
} else return Promise.reject() } else return Promise.reject();
} catch (reason1) { } catch (reason1) {
return await Promise.reject(reason1) return await Promise.reject(reason1);
} }
} }
} };
const [selectedManga, setSelectedManga] = useState< const [selectedManga, setSelectedManga] = useState<
MinimalManga | undefined MinimalManga | undefined
>(undefined) >(undefined);
const [mangaDetailOpen, setMangaDetailOpen] = useState(false) const [mangaDetailOpen, setMangaDetailOpen] = useState(false);
const [mangaDownloadDrawerOpen, setMangaDownloadDrawerOpen] = const [mangaDownloadDrawerOpen, setMangaDownloadDrawerOpen] =
useState(false) useState(false);
function openMangaDetail(manga: MinimalManga) { function openMangaDetail(manga: MinimalManga) {
setSelectedManga(manga) setSelectedManga(manga);
setMangaDetailOpen(true) setMangaDetailOpen(true);
} }
function openMangaDownloadDrawer() { function openMangaDownloadDrawer() {
setMangaDetailOpen(false) setMangaDetailOpen(false);
setMangaDownloadDrawerOpen(true) setMangaDownloadDrawerOpen(true);
} }
return ( return (
<Modal open={props.open} onClose={() => props.setOpen(false)}> <Modal
open={props.open}
onClose={() => props.setOpen(false)}>
<ModalDialog sx={{ width: '90vw' }}> <ModalDialog sx={{ width: '90vw' }}>
<ModalClose /> <ModalClose />
<Stepper> <Stepper>
<Step <Step
orientation={'vertical'} orientation={'vertical'}
indicator={<StepIndicator>1</StepIndicator>} indicator={<StepIndicator>1</StepIndicator>}>
>
<Typography level={'title-lg'}> <Typography level={'title-lg'}>
Select a connector Select a connector
</Typography> </Typography>
@@ -100,8 +102,7 @@ export function Search(props: SearchModalProps): ReactNode {
{MangaConnectors.map((c) => ( {MangaConnectors.map((c) => (
<ListItem <ListItem
key={c.key} key={c.key}
onClick={() => setSelectedConnector(c)} onClick={() => setSelectedConnector(c)}>
>
<ListItemDecorator> <ListItemDecorator>
<MangaConnectorIcon <MangaConnectorIcon
mangaConnector={c} mangaConnector={c}
@@ -112,8 +113,7 @@ export function Search(props: SearchModalProps): ReactNode {
c.key == selectedConnector?.key c.key == selectedConnector?.key
? { fontWeight: 'bold' } ? { fontWeight: 'bold' }
: {} : {}
} }>
>
{c.name} {c.name}
</Typography> </Typography>
</ListItem> </ListItem>
@@ -122,8 +122,7 @@ export function Search(props: SearchModalProps): ReactNode {
</Step> </Step>
<Step <Step
orientation={'vertical'} orientation={'vertical'}
indicator={<StepIndicator>2</StepIndicator>} indicator={<StepIndicator>2</StepIndicator>}>
>
<Typography level={'title-lg'}> <Typography level={'title-lg'}>
Enter a search term or URL Enter a search term or URL
</Typography> </Typography>
@@ -154,19 +153,19 @@ export function Search(props: SearchModalProps): ReactNode {
/> />
</ModalDialog> </ModalDialog>
</Modal> </Modal>
) );
} }
export interface SearchModalProps { export interface SearchModalProps {
open: boolean open: boolean;
setOpen: Dispatch<boolean> setOpen: Dispatch<boolean>;
} }
function isUrl(str: string): boolean { function isUrl(str: string): boolean {
try { try {
new URL(str) new URL(str);
return true return true;
} catch { } catch {
return false return false;
} }
} }

View File

@@ -30,8 +30,8 @@ import {
TrangaSettings, TrangaSettings,
Worker, Worker,
WorkerExecutionState, WorkerExecutionState,
} from './data-contracts' } from './data-contracts';
import { ContentType, HttpClient, RequestParams } from './http-client' import { ContentType, HttpClient, RequestParams } from './http-client';
export class V2< export class V2<
SecurityDataType = unknown, SecurityDataType = unknown,
@@ -50,7 +50,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -66,7 +66,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -81,7 +81,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -95,7 +95,7 @@ export class V2<
path: `/v2/FileLibrary/${fileLibraryId}`, path: `/v2/FileLibrary/${fileLibraryId}`,
method: 'DELETE', method: 'DELETE',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -115,7 +115,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -135,7 +135,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -150,7 +150,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -169,7 +169,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -187,7 +187,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -204,7 +204,7 @@ export class V2<
path: `/v2/LibraryConnector/${libraryConnectorId}`, path: `/v2/LibraryConnector/${libraryConnectorId}`,
method: 'DELETE', method: 'DELETE',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -218,7 +218,7 @@ export class V2<
path: `/v2/Maintenance/CleanupNoDownloadManga`, path: `/v2/Maintenance/CleanupNoDownloadManga`,
method: 'POST', method: 'POST',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -233,7 +233,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -248,7 +248,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -263,7 +263,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -280,7 +280,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -295,7 +295,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -309,7 +309,7 @@ export class V2<
path: `/v2/Manga/${mangaId}`, path: `/v2/Manga/${mangaId}`,
method: 'DELETE', method: 'DELETE',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -327,7 +327,7 @@ export class V2<
path: `/v2/Manga/${mangaIdFrom}/MergeInto/${mangaIdInto}`, path: `/v2/Manga/${mangaIdFrom}/MergeInto/${mangaIdInto}`,
method: 'PATCH', method: 'PATCH',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -343,12 +343,12 @@ export class V2<
* If width is provided, height needs to also be provided * If width is provided, height needs to also be provided
* @format int32 * @format int32
*/ */
width?: number width?: number;
/** /**
* If height is provided, width needs to also be provided * If height is provided, width needs to also be provided
* @format int32 * @format int32
*/ */
height?: number height?: number;
}, },
params: RequestParams = {} params: RequestParams = {}
) => ) =>
@@ -358,7 +358,7 @@ export class V2<
query: query, query: query,
format: 'blob', format: 'blob',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -373,7 +373,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -391,7 +391,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -409,7 +409,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -427,7 +427,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -445,7 +445,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -465,7 +465,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -483,7 +483,7 @@ export class V2<
path: `/v2/Manga/${mangaId}/ChangeLibrary/${libraryId}`, path: `/v2/Manga/${mangaId}/ChangeLibrary/${libraryId}`,
method: 'POST', method: 'POST',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -502,7 +502,7 @@ export class V2<
path: `/v2/Manga/${mangaId}/SetAsDownloadFrom/${mangaConnectorName}/${isRequested}`, path: `/v2/Manga/${mangaId}/SetAsDownloadFrom/${mangaConnectorName}/${isRequested}`,
method: 'POST', method: 'POST',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -521,7 +521,7 @@ export class V2<
method: 'POST', method: 'POST',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -536,7 +536,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -551,7 +551,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -566,7 +566,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -584,7 +584,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -599,7 +599,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -614,7 +614,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -632,7 +632,7 @@ export class V2<
path: `/v2/MangaConnector/${mangaConnectorName}/SetEnabled/${enabled}`, path: `/v2/MangaConnector/${mangaConnectorName}/SetEnabled/${enabled}`,
method: 'PATCH', method: 'PATCH',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -647,7 +647,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -662,7 +662,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -684,7 +684,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -706,7 +706,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -724,7 +724,7 @@ export class V2<
path: `/v2/MetadataFetcher/${metadataFetcherName}/Unlink/${mangaId}`, path: `/v2/MetadataFetcher/${metadataFetcherName}/Unlink/${mangaId}`,
method: 'POST', method: 'POST',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -739,7 +739,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* @description Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent * @description Formatting placeholders: "%title" and "%text" can be placed in url, header-values and body and will be replaced when notifications are sent
* *
@@ -759,7 +759,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -774,7 +774,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -788,7 +788,7 @@ export class V2<
path: `/v2/NotificationConnector/${name}`, path: `/v2/NotificationConnector/${name}`,
method: 'DELETE', method: 'DELETE',
...params, ...params,
}) });
/** /**
* @description Priority needs to be between 0 and 10 * @description Priority needs to be between 0 and 10
* *
@@ -808,7 +808,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* @description Priority needs to be between 1 and 5 * @description Priority needs to be between 1 and 5
* *
@@ -828,7 +828,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* @description https://pushover.net/api * @description https://pushover.net/api
* *
@@ -848,7 +848,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -863,7 +863,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -878,7 +878,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -896,7 +896,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -911,7 +911,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -929,7 +929,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -948,7 +948,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -965,7 +965,7 @@ export class V2<
type: ContentType.Json, type: ContentType.Json,
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -980,7 +980,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -994,7 +994,7 @@ export class V2<
path: `/v2/Settings/UserAgent`, path: `/v2/Settings/UserAgent`,
method: 'GET', method: 'GET',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1013,7 +1013,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1027,7 +1027,7 @@ export class V2<
path: `/v2/Settings/UserAgent`, path: `/v2/Settings/UserAgent`,
method: 'DELETE', method: 'DELETE',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1040,17 +1040,17 @@ export class V2<
this.request< this.request<
{ {
/** @format int32 */ /** @format int32 */
Default?: number Default?: number;
/** @format int32 */ /** @format int32 */
MangaDexFeed?: number MangaDexFeed?: number;
/** @format int32 */ /** @format int32 */
MangaImage?: number MangaImage?: number;
/** @format int32 */ /** @format int32 */
MangaCover?: number MangaCover?: number;
/** @format int32 */ /** @format int32 */
MangaDexImage?: number MangaDexImage?: number;
/** @format int32 */ /** @format int32 */
MangaInfo?: number MangaInfo?: number;
}, },
any any
>({ >({
@@ -1058,7 +1058,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* @description <h1>NOT IMPLEMENTED</h1> * @description <h1>NOT IMPLEMENTED</h1>
* *
@@ -1072,7 +1072,7 @@ export class V2<
path: `/v2/Settings/RequestLimits`, path: `/v2/Settings/RequestLimits`,
method: 'PATCH', method: 'PATCH',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1087,7 +1087,7 @@ export class V2<
method: 'DELETE', method: 'DELETE',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1109,7 +1109,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1129,7 +1129,7 @@ export class V2<
method: 'DELETE', method: 'DELETE',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1143,7 +1143,7 @@ export class V2<
path: `/v2/Settings/ImageCompressionLevel`, path: `/v2/Settings/ImageCompressionLevel`,
method: 'GET', method: 'GET',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1160,7 +1160,7 @@ export class V2<
path: `/v2/Settings/ImageCompressionLevel/${level}`, path: `/v2/Settings/ImageCompressionLevel/${level}`,
method: 'PATCH', method: 'PATCH',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1174,7 +1174,7 @@ export class V2<
path: `/v2/Settings/BWImages`, path: `/v2/Settings/BWImages`,
method: 'GET', method: 'GET',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1191,7 +1191,7 @@ export class V2<
path: `/v2/Settings/BWImages/${enabled}`, path: `/v2/Settings/BWImages/${enabled}`,
method: 'PATCH', method: 'PATCH',
...params, ...params,
}) });
/** /**
* @description Placeholders: %M Obj Name %V Volume %C Chapter %T Title %A Author (first in list) %I Chapter Internal ID %i Obj Internal ID %Y Year (Obj) ?_(...) replace _ with a value from above: Everything inside the braces will only be added if the value of %_ is not null * @description Placeholders: %M Obj Name %V Volume %C Chapter %T Title %A Author (first in list) %I Chapter Internal ID %i Obj Internal ID %Y Year (Obj) ?_(...) replace _ with a value from above: Everything inside the braces will only be added if the value of %_ is not null
* *
@@ -1205,7 +1205,7 @@ export class V2<
path: `/v2/Settings/ChapterNamingScheme`, path: `/v2/Settings/ChapterNamingScheme`,
method: 'GET', method: 'GET',
...params, ...params,
}) });
/** /**
* @description Placeholders: %M Obj Name %V Volume %C Chapter %T Title %A Author (first in list) %Y Year (Obj) ?_(...) replace _ with a value from above: Everything inside the braces will only be added if the value of %_ is not null * @description Placeholders: %M Obj Name %V Volume %C Chapter %T Title %A Author (first in list) %Y Year (Obj) ?_(...) replace _ with a value from above: Everything inside the braces will only be added if the value of %_ is not null
* *
@@ -1224,7 +1224,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1243,7 +1243,7 @@ export class V2<
body: data, body: data,
type: ContentType.Json, type: ContentType.Json,
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1257,7 +1257,7 @@ export class V2<
path: `/v2/Settings/FlareSolverr/Url`, path: `/v2/Settings/FlareSolverr/Url`,
method: 'DELETE', method: 'DELETE',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1271,7 +1271,7 @@ export class V2<
path: `/v2/Settings/FlareSolverr/Test`, path: `/v2/Settings/FlareSolverr/Test`,
method: 'POST', method: 'POST',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1285,7 +1285,7 @@ export class V2<
path: `/v2/Settings/DownloadLanguage`, path: `/v2/Settings/DownloadLanguage`,
method: 'GET', method: 'GET',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1302,7 +1302,7 @@ export class V2<
path: `/v2/Settings/DownloadLanguage/${language}`, path: `/v2/Settings/DownloadLanguage/${language}`,
method: 'PATCH', method: 'PATCH',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1317,7 +1317,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1332,7 +1332,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1350,7 +1350,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1365,7 +1365,7 @@ export class V2<
method: 'GET', method: 'GET',
format: 'json', format: 'json',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1379,7 +1379,7 @@ export class V2<
path: `/v2/Worker/${workerId}`, path: `/v2/Worker/${workerId}`,
method: 'DELETE', method: 'DELETE',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1393,7 +1393,7 @@ export class V2<
path: `/v2/Worker/${workerId}/Start`, path: `/v2/Worker/${workerId}/Start`,
method: 'POST', method: 'POST',
...params, ...params,
}) });
/** /**
* No description * No description
* *
@@ -1407,5 +1407,5 @@ export class V2<
path: `/v2/Worker/${workerId}/Stop`, path: `/v2/Worker/${workerId}/Stop`,
method: 'POST', method: 'POST',
...params, ...params,
}) });
} }

View File

@@ -47,12 +47,12 @@ export interface AltTitle {
* Language of the Title * Language of the Title
* @minLength 1 * @minLength 1
*/ */
language: string language: string;
/** /**
* Title * Title
* @minLength 1 * @minLength 1
*/ */
title: string title: string;
} }
/** The API.Schema.MangaContext.Author DTO */ /** The API.Schema.MangaContext.Author DTO */
@@ -61,13 +61,13 @@ export interface Author {
* Name of the Author. * Name of the Author.
* @minLength 1 * @minLength 1
*/ */
name: string name: string;
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
/** API.Schema.MangaContext.Chapter DTO */ /** API.Schema.MangaContext.Chapter DTO */
@@ -76,32 +76,32 @@ export interface Chapter {
* Identifier of the Manga this Chapter belongs to * Identifier of the Manga this Chapter belongs to
* @minLength 1 * @minLength 1
*/ */
mangaId: string mangaId: string;
/** /**
* Volume number * Volume number
* @format int32 * @format int32
*/ */
volume: number volume: number;
/** /**
* Chapter number * Chapter number
* @minLength 1 * @minLength 1
*/ */
chapterNumber: string chapterNumber: string;
/** /**
* Title of the Chapter * Title of the Chapter
* @minLength 1 * @minLength 1
*/ */
title: string title: string;
/** Whether Chapter is Downloaded (on disk) */ /** Whether Chapter is Downloaded (on disk) */
downloaded: boolean downloaded: boolean;
/** Ids of the Manga on MangaConnectors */ /** Ids of the Manga on MangaConnectors */
mangaConnectorIds: MangaConnectorId[] mangaConnectorIds: MangaConnectorId[];
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
export interface FileLibrary { export interface FileLibrary {
@@ -109,45 +109,45 @@ export interface FileLibrary {
* @minLength 0 * @minLength 0
* @maxLength 256 * @maxLength 256
*/ */
basePath: string basePath: string;
/** /**
* @minLength 0 * @minLength 0
* @maxLength 512 * @maxLength 512
*/ */
libraryName: string libraryName: string;
/** /**
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
export interface GotifyRecord { export interface GotifyRecord {
name?: string | null name?: string | null;
endpoint?: string | null endpoint?: string | null;
appToken?: string | null appToken?: string | null;
/** @format int32 */ /** @format int32 */
priority?: number priority?: number;
} }
export interface LibraryConnector { export interface LibraryConnector {
libraryType: LibraryType libraryType: LibraryType;
/** /**
* @format uri * @format uri
* @minLength 0 * @minLength 0
* @maxLength 256 * @maxLength 256
*/ */
baseUrl: string baseUrl: string;
/** /**
* @minLength 0 * @minLength 0
* @maxLength 256 * @maxLength 256
*/ */
auth: string auth: string;
/** /**
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
/** API.Schema.MangaContext.Link DTO */ /** API.Schema.MangaContext.Link DTO */
@@ -156,18 +156,18 @@ export interface Link {
* Name of the Provider * Name of the Provider
* @minLength 1 * @minLength 1
*/ */
provider: string provider: string;
/** /**
* Url * Url
* @minLength 1 * @minLength 1
*/ */
url: string url: string;
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
/** API.Schema.MangaContext.Manga DTO */ /** API.Schema.MangaContext.Manga DTO */
@@ -176,67 +176,67 @@ export interface Manga {
* Chapter cutoff for Downloads (Chapters before this will not be downloaded) * Chapter cutoff for Downloads (Chapters before this will not be downloaded)
* @format float * @format float
*/ */
ignoreChaptersBefore: number ignoreChaptersBefore: number;
/** /**
* Release Year * Release Year
* @format int32 * @format int32
*/ */
year?: number | null year?: number | null;
/** Release Language */ /** Release Language */
originalLanguage?: string | null originalLanguage?: string | null;
/** Keys of ChapterDTOs */ /** Keys of ChapterDTOs */
chapterIds: string[] chapterIds: string[];
/** Author-names */ /** Author-names */
authors: Author[] authors: Author[];
/** Manga Tags */ /** Manga Tags */
tags: string[] tags: string[];
/** Links for more Metadata */ /** Links for more Metadata */
links: Link[] links: Link[];
/** Alt Titles of Manga */ /** Alt Titles of Manga */
altTitles: AltTitle[] altTitles: AltTitle[];
/** /**
* Id of the Library the Manga gets downloaded to * Id of the Library the Manga gets downloaded to
* @minLength 1 * @minLength 1
*/ */
fileLibraryId: string fileLibraryId: string;
/** /**
* Name of the Manga * Name of the Manga
* @minLength 1 * @minLength 1
*/ */
name: string name: string;
/** /**
* Description of the Manga * Description of the Manga
* @minLength 1 * @minLength 1
*/ */
description: string description: string;
releaseStatus: MangaReleaseStatus releaseStatus: MangaReleaseStatus;
/** Ids of the Manga on MangaConnectors */ /** Ids of the Manga on MangaConnectors */
mangaConnectorIds: MangaConnectorId[] mangaConnectorIds: MangaConnectorId[];
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
export interface MangaConnector { export interface MangaConnector {
name?: string | null name?: string | null;
/** Whether Connector is used for Searches and Downloads */ /** Whether Connector is used for Searches and Downloads */
enabled: boolean enabled: boolean;
/** Languages supported by the Connector */ /** Languages supported by the Connector */
supportedLanguages: string[] supportedLanguages: string[];
/** /**
* Url of the Website Icon * Url of the Website Icon
* @minLength 1 * @minLength 1
*/ */
iconUrl: string iconUrl: string;
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
/** API.Schema.MangaContext.MangaConnectorId`1 DTO */ /** API.Schema.MangaContext.MangaConnectorId`1 DTO */
@@ -245,36 +245,36 @@ export interface MangaConnectorId {
* Name of the Connector * Name of the Connector
* @minLength 1 * @minLength 1
*/ */
mangaConnectorName: string mangaConnectorName: string;
/** /**
* Key of the referenced DTO * Key of the referenced DTO
* @minLength 1 * @minLength 1
*/ */
foreignKey: string foreignKey: string;
/** Website Link for reference, if any */ /** Website Link for reference, if any */
websiteUrl?: string | null websiteUrl?: string | null;
/** Whether this Link is used for downloads */ /** Whether this Link is used for downloads */
useForDownload: boolean useForDownload: boolean;
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
export interface MetadataEntry { export interface MetadataEntry {
mangaId?: string | null mangaId?: string | null;
metadataFetcherName?: string | null metadataFetcherName?: string | null;
identifier?: string | null identifier?: string | null;
} }
export interface MetadataSearchResult { export interface MetadataSearchResult {
identifier?: string | null identifier?: string | null;
name?: string | null name?: string | null;
url?: string | null url?: string | null;
description?: string | null description?: string | null;
coverUrl?: string | null coverUrl?: string | null;
} }
/** Shortened Version of API.Controllers.DTOs.Manga */ /** Shortened Version of API.Controllers.DTOs.Manga */
@@ -283,21 +283,21 @@ export interface MinimalManga {
* Name of the Manga * Name of the Manga
* @minLength 1 * @minLength 1
*/ */
name: string name: string;
/** /**
* Description of the Manga * Description of the Manga
* @minLength 1 * @minLength 1
*/ */
description: string description: string;
releaseStatus: MangaReleaseStatus releaseStatus: MangaReleaseStatus;
/** Ids of the Manga on MangaConnectors */ /** Ids of the Manga on MangaConnectors */
mangaConnectorIds: MangaConnectorId[] mangaConnectorIds: MangaConnectorId[];
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }
export interface NotificationConnector { export interface NotificationConnector {
@@ -305,59 +305,59 @@ export interface NotificationConnector {
* @minLength 0 * @minLength 0
* @maxLength 64 * @maxLength 64
*/ */
name: string name: string;
/** /**
* @format uri * @format uri
* @minLength 0 * @minLength 0
* @maxLength 2048 * @maxLength 2048
*/ */
url: string url: string;
headers: Record<string, string> headers: Record<string, string>;
/** /**
* @minLength 0 * @minLength 0
* @maxLength 8 * @maxLength 8
*/ */
httpMethod: string httpMethod: string;
/** /**
* @minLength 0 * @minLength 0
* @maxLength 4096 * @maxLength 4096
*/ */
body: string body: string;
} }
export interface NtfyRecord { export interface NtfyRecord {
name?: string | null name?: string | null;
endpoint?: string | null endpoint?: string | null;
username?: string | null username?: string | null;
password?: string | null password?: string | null;
topic?: string | null topic?: string | null;
/** @format int32 */ /** @format int32 */
priority?: number priority?: number;
} }
export interface ProblemDetails { export interface ProblemDetails {
type?: string | null type?: string | null;
title?: string | null title?: string | null;
/** @format int32 */ /** @format int32 */
status?: number | null status?: number | null;
detail?: string | null detail?: string | null;
instance?: string | null instance?: string | null;
[key: string]: any [key: string]: any;
} }
export interface PushoverRecord { export interface PushoverRecord {
name?: string | null name?: string | null;
appToken?: string | null appToken?: string | null;
user?: string | null user?: string | null;
} }
export interface TrangaSettings { export interface TrangaSettings {
downloadLocation?: string | null downloadLocation?: string | null;
userAgent?: string | null userAgent?: string | null;
/** @format int32 */ /** @format int32 */
imageCompression?: number imageCompression?: number;
blackWhiteImages?: boolean blackWhiteImages?: boolean;
flareSolverrUrl?: string | null flareSolverrUrl?: string | null;
/** /**
* Placeholders: * Placeholders:
* %M Obj Name * %M Obj Name
@@ -372,39 +372,39 @@ export interface TrangaSettings {
* ?_(...) replace _ with a value from above: * ?_(...) replace _ with a value from above:
* Everything inside the braces will only be added if the value of %_ is not null * Everything inside the braces will only be added if the value of %_ is not null
*/ */
chapterNamingScheme?: string | null chapterNamingScheme?: string | null;
/** @format int32 */ /** @format int32 */
workCycleTimeoutMs?: number workCycleTimeoutMs?: number;
requestLimits?: { requestLimits?: {
/** @format int32 */ /** @format int32 */
Default?: number Default?: number;
/** @format int32 */ /** @format int32 */
MangaDexFeed?: number MangaDexFeed?: number;
/** @format int32 */ /** @format int32 */
MangaImage?: number MangaImage?: number;
/** @format int32 */ /** @format int32 */
MangaCover?: number MangaCover?: number;
/** @format int32 */ /** @format int32 */
MangaDexImage?: number MangaDexImage?: number;
/** @format int32 */ /** @format int32 */
MangaInfo?: number MangaInfo?: number;
} | null } | null;
downloadLanguage?: string | null downloadLanguage?: string | null;
} }
/** API.Workers.BaseWorker DTO */ /** API.Workers.BaseWorker DTO */
export interface Worker { export interface Worker {
/** Workers this worker depends on having ran. */ /** Workers this worker depends on having ran. */
dependencies: string[] dependencies: string[];
/** Workers that have not yet ran, that need to run for this Worker to run. */ /** Workers that have not yet ran, that need to run for this Worker to run. */
missingDependencies: string[] missingDependencies: string[];
/** Worker can run. */ /** Worker can run. */
dependenciesFulfilled: boolean dependenciesFulfilled: boolean;
state: WorkerExecutionState state: WorkerExecutionState;
/** /**
* Unique Identifier of the DTO * Unique Identifier of the DTO
* @minLength 16 * @minLength 16
* @maxLength 64 * @maxLength 64
*/ */
key: string key: string;
} }

View File

@@ -10,49 +10,49 @@
* --------------------------------------------------------------- * ---------------------------------------------------------------
*/ */
export type QueryParamsType = Record<string | number, any> export type QueryParamsType = Record<string | number, any>;
export type ResponseFormat = keyof Omit<Body, 'body' | 'bodyUsed'> export type ResponseFormat = keyof Omit<Body, 'body' | 'bodyUsed'>;
export interface FullRequestParams extends Omit<RequestInit, 'body'> { export interface FullRequestParams extends Omit<RequestInit, 'body'> {
/** set parameter to `true` for call `securityWorker` for this request */ /** set parameter to `true` for call `securityWorker` for this request */
secure?: boolean secure?: boolean;
/** request path */ /** request path */
path: string path: string;
/** content type of request body */ /** content type of request body */
type?: ContentType type?: ContentType;
/** query params */ /** query params */
query?: QueryParamsType query?: QueryParamsType;
/** format of response (i.e. response.json() -> format: "json") */ /** format of response (i.e. response.json() -> format: "json") */
format?: ResponseFormat format?: ResponseFormat;
/** request body */ /** request body */
body?: unknown body?: unknown;
/** base url */ /** base url */
baseUrl?: string baseUrl?: string;
/** request cancellation token */ /** request cancellation token */
cancelToken?: CancelToken cancelToken?: CancelToken;
} }
export type RequestParams = Omit< export type RequestParams = Omit<
FullRequestParams, FullRequestParams,
'body' | 'method' | 'query' | 'path' 'body' | 'method' | 'query' | 'path'
> >;
export interface ApiConfig<SecurityDataType = unknown> { export interface ApiConfig<SecurityDataType = unknown> {
baseUrl?: string baseUrl?: string;
baseApiParams?: Omit<RequestParams, 'baseUrl' | 'cancelToken' | 'signal'> baseApiParams?: Omit<RequestParams, 'baseUrl' | 'cancelToken' | 'signal'>;
securityWorker?: ( securityWorker?: (
securityData: SecurityDataType | null securityData: SecurityDataType | null
) => Promise<RequestParams | void> | RequestParams | void ) => Promise<RequestParams | void> | RequestParams | void;
customFetch?: typeof fetch customFetch?: typeof fetch;
} }
export interface HttpResponse<D extends unknown, E extends unknown = unknown> export interface HttpResponse<D extends unknown, E extends unknown = unknown>
extends Response { extends Response {
data: D data: D;
error: E error: E;
} }
type CancelToken = Symbol | string | number type CancelToken = Symbol | string | number;
export enum ContentType { export enum ContentType {
Json = 'application/json', Json = 'application/json',
@@ -63,59 +63,59 @@ export enum ContentType {
} }
export class HttpClient<SecurityDataType = unknown> { export class HttpClient<SecurityDataType = unknown> {
public baseUrl: string = '' public baseUrl: string = '';
private securityData: SecurityDataType | null = null private securityData: SecurityDataType | null = null;
private securityWorker?: ApiConfig<SecurityDataType>['securityWorker'] private securityWorker?: ApiConfig<SecurityDataType>['securityWorker'];
private abortControllers = new Map<CancelToken, AbortController>() private abortControllers = new Map<CancelToken, AbortController>();
private customFetch = (...fetchParams: Parameters<typeof fetch>) => private customFetch = (...fetchParams: Parameters<typeof fetch>) =>
fetch(...fetchParams) fetch(...fetchParams);
private baseApiParams: RequestParams = { private baseApiParams: RequestParams = {
credentials: 'same-origin', credentials: 'same-origin',
headers: {}, headers: {},
redirect: 'follow', redirect: 'follow',
referrerPolicy: 'no-referrer', referrerPolicy: 'no-referrer',
} };
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) { constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
Object.assign(this, apiConfig) Object.assign(this, apiConfig);
} }
public setSecurityData = (data: SecurityDataType | null) => { public setSecurityData = (data: SecurityDataType | null) => {
this.securityData = data this.securityData = data;
} };
protected encodeQueryParam(key: string, value: any) { protected encodeQueryParam(key: string, value: any) {
const encodedKey = encodeURIComponent(key) const encodedKey = encodeURIComponent(key);
return `${encodedKey}=${encodeURIComponent(typeof value === 'number' ? value : `${value}`)}` return `${encodedKey}=${encodeURIComponent(typeof value === 'number' ? value : `${value}`)}`;
} }
protected addQueryParam(query: QueryParamsType, key: string) { protected addQueryParam(query: QueryParamsType, key: string) {
return this.encodeQueryParam(key, query[key]) return this.encodeQueryParam(key, query[key]);
} }
protected addArrayQueryParam(query: QueryParamsType, key: string) { protected addArrayQueryParam(query: QueryParamsType, key: string) {
const value = query[key] const value = query[key];
return value.map((v: any) => this.encodeQueryParam(key, v)).join('&') return value.map((v: any) => this.encodeQueryParam(key, v)).join('&');
} }
protected toQueryString(rawQuery?: QueryParamsType): string { protected toQueryString(rawQuery?: QueryParamsType): string {
const query = rawQuery || {} const query = rawQuery || {};
const keys = Object.keys(query).filter( const keys = Object.keys(query).filter(
(key) => 'undefined' !== typeof query[key] (key) => 'undefined' !== typeof query[key]
) );
return keys return keys
.map((key) => .map((key) =>
Array.isArray(query[key]) Array.isArray(query[key])
? this.addArrayQueryParam(query, key) ? this.addArrayQueryParam(query, key)
: this.addQueryParam(query, key) : this.addQueryParam(query, key)
) )
.join('&') .join('&');
} }
protected addQueryParams(rawQuery?: QueryParamsType): string { protected addQueryParams(rawQuery?: QueryParamsType): string {
const queryString = this.toQueryString(rawQuery) const queryString = this.toQueryString(rawQuery);
return queryString ? `?${queryString}` : '' return queryString ? `?${queryString}` : '';
} }
private contentFormatters: Record<ContentType, (input: any) => any> = { private contentFormatters: Record<ContentType, (input: any) => any> = {
@@ -135,7 +135,7 @@ export class HttpClient<SecurityDataType = unknown> {
: input, : input,
[ContentType.FormData]: (input: any) => [ContentType.FormData]: (input: any) =>
Object.keys(input || {}).reduce((formData, key) => { Object.keys(input || {}).reduce((formData, key) => {
const property = input[key] const property = input[key];
formData.append( formData.append(
key, key,
property instanceof Blob property instanceof Blob
@@ -143,11 +143,11 @@ export class HttpClient<SecurityDataType = unknown> {
: typeof property === 'object' && property !== null : typeof property === 'object' && property !== null
? JSON.stringify(property) ? JSON.stringify(property)
: `${property}` : `${property}`
) );
return formData return formData;
}, new FormData()), }, new FormData()),
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input), [ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
} };
protected mergeRequestParams( protected mergeRequestParams(
params1: RequestParams, params1: RequestParams,
@@ -162,33 +162,33 @@ export class HttpClient<SecurityDataType = unknown> {
...(params1.headers || {}), ...(params1.headers || {}),
...((params2 && params2.headers) || {}), ...((params2 && params2.headers) || {}),
}, },
} };
} }
protected createAbortSignal = ( protected createAbortSignal = (
cancelToken: CancelToken cancelToken: CancelToken
): AbortSignal | undefined => { ): AbortSignal | undefined => {
if (this.abortControllers.has(cancelToken)) { if (this.abortControllers.has(cancelToken)) {
const abortController = this.abortControllers.get(cancelToken) const abortController = this.abortControllers.get(cancelToken);
if (abortController) { if (abortController) {
return abortController.signal return abortController.signal;
} }
return void 0 return void 0;
} }
const abortController = new AbortController() const abortController = new AbortController();
this.abortControllers.set(cancelToken, abortController) this.abortControllers.set(cancelToken, abortController);
return abortController.signal return abortController.signal;
} };
public abortRequest = (cancelToken: CancelToken) => { public abortRequest = (cancelToken: CancelToken) => {
const abortController = this.abortControllers.get(cancelToken) const abortController = this.abortControllers.get(cancelToken);
if (abortController) { if (abortController) {
abortController.abort() abortController.abort();
this.abortControllers.delete(cancelToken) this.abortControllers.delete(cancelToken);
} }
} };
public request = async <T = any, E = any>({ public request = async <T = any, E = any>({
body, body,
@@ -207,12 +207,12 @@ export class HttpClient<SecurityDataType = unknown> {
: this.baseApiParams.secure) && : this.baseApiParams.secure) &&
this.securityWorker && this.securityWorker &&
(await this.securityWorker(this.securityData))) || (await this.securityWorker(this.securityData))) ||
{} {};
const requestParams = this.mergeRequestParams(params, secureParams) const requestParams = this.mergeRequestParams(params, secureParams);
const queryString = query && this.toQueryString(query) const queryString = query && this.toQueryString(query);
const payloadFormatter = const payloadFormatter =
this.contentFormatters[type || ContentType.Json] this.contentFormatters[type || ContentType.Json];
const responseFormat = format || requestParams.format const responseFormat = format || requestParams.format;
return this.customFetch( return this.customFetch(
`${baseUrl || this.baseUrl || ''}${path}${queryString ? `?${queryString}` : ''}`, `${baseUrl || this.baseUrl || ''}${path}${queryString ? `?${queryString}` : ''}`,
@@ -234,32 +234,32 @@ export class HttpClient<SecurityDataType = unknown> {
: payloadFormatter(body), : payloadFormatter(body),
} }
).then(async (response) => { ).then(async (response) => {
const r = response.clone() as HttpResponse<T, E> const r = response.clone() as HttpResponse<T, E>;
r.data = null as unknown as T r.data = null as unknown as T;
r.error = null as unknown as E r.error = null as unknown as E;
const data = !responseFormat const data = !responseFormat
? r ? r
: await response[responseFormat]() : await response[responseFormat]()
.then((data) => { .then((data) => {
if (r.ok) { if (r.ok) {
r.data = data r.data = data;
} else { } else {
r.error = data r.error = data;
} }
return r return r;
}) })
.catch((e) => { .catch((e) => {
r.error = e r.error = e;
return r return r;
}) });
if (cancelToken) { if (cancelToken) {
this.abortControllers.delete(cancelToken) this.abortControllers.delete(cancelToken);
} }
if (!response.ok) throw data if (!response.ok) throw data;
return data return data;
}) });
} };
} }

View File

@@ -1,20 +1,20 @@
import { V2 } from '../api/V2.ts' import { V2 } from '../api/V2.ts';
import { createContext, ReactNode, useEffect, useState } from 'react' import { createContext, ReactNode, useEffect, useState } from 'react';
import { ApiConfig } from '../api/http-client.ts' import { ApiConfig } from '../api/http-client.ts';
export const ApiContext = createContext<V2>(new V2()) export const ApiContext = createContext<V2>(new V2());
export default function ApiProvider({ export default function ApiProvider({
apiConfig, apiConfig,
children, children,
}: { }: {
apiConfig: ApiConfig apiConfig: ApiConfig;
children: ReactNode children: ReactNode;
}) { }) {
const [api, setApi] = useState<V2>(new V2(apiConfig)) const [api, setApi] = useState<V2>(new V2(apiConfig));
useEffect(() => { useEffect(() => {
setApi(new V2(apiConfig)) setApi(new V2(apiConfig));
}, [apiConfig]) }, [apiConfig]);
return <ApiContext value={api}>{children}</ApiContext> return <ApiContext value={api}>{children}</ApiContext>;
} }

View File

@@ -4,28 +4,28 @@ import {
useContext, useContext,
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react';
import { FileLibrary } from '../api/data-contracts.ts' import { FileLibrary } from '../api/data-contracts.ts';
import { ApiContext } from './ApiContext.tsx' import { ApiContext } from './ApiContext.tsx';
export const FileLibraryContext = createContext<FileLibrary[]>([]) export const FileLibraryContext = createContext<FileLibrary[]>([]);
export default function LibraryProvider({ export default function LibraryProvider({
children, children,
}: { }: {
children: ReactNode children: ReactNode;
}): ReactNode { }): ReactNode {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const [state, setState] = useState<FileLibrary[]>([]) const [state, setState] = useState<FileLibrary[]>([]);
useEffect(() => { useEffect(() => {
Api.fileLibraryList().then((result) => { Api.fileLibraryList().then((result) => {
if (result.ok) { if (result.ok) {
setState(result.data) setState(result.data);
} }
}) });
}, [Api]) }, [Api]);
return <FileLibraryContext value={state}>{children}</FileLibraryContext> return <FileLibraryContext value={state}>{children}</FileLibraryContext>;
} }

View File

@@ -4,30 +4,30 @@ import {
useContext, useContext,
useEffect, useEffect,
useState, useState,
} from 'react' } from 'react';
import { MangaConnector } from '../api/data-contracts.ts' import { MangaConnector } from '../api/data-contracts.ts';
import { ApiContext } from './ApiContext.tsx' import { ApiContext } from './ApiContext.tsx';
export const MangaConnectorContext = createContext<MangaConnector[]>([]) export const MangaConnectorContext = createContext<MangaConnector[]>([]);
export default function MangaConnectorProvider({ export default function MangaConnectorProvider({
children, children,
}: { }: {
children: ReactNode children: ReactNode;
}) { }) {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
const [state, setState] = useState<MangaConnector[]>([]) const [state, setState] = useState<MangaConnector[]>([]);
useEffect(() => { useEffect(() => {
Api.mangaConnectorList().then((result) => { Api.mangaConnectorList().then((result) => {
if (result.ok) { if (result.ok) {
setState(result.data) setState(result.data);
} }
}) });
}, [Api]) }, [Api]);
return ( return (
<MangaConnectorContext value={state}>{children}</MangaConnectorContext> <MangaConnectorContext value={state}>{children}</MangaConnectorContext>
) );
} }

View File

@@ -1,43 +1,43 @@
import { createContext, ReactNode, useContext } from 'react' import { createContext, ReactNode, useContext } from 'react';
import { ApiContext } from './ApiContext.tsx' import { ApiContext } from './ApiContext.tsx';
import { Manga } from '../api/data-contracts.ts' import { Manga } from '../api/data-contracts.ts';
import { V2 } from '../api/V2.ts' import { V2 } from '../api/V2.ts';
export const MangaContext = createContext<M>({ export const MangaContext = createContext<M>({
GetManga: () => Promise.reject(), GetManga: () => Promise.reject(),
}) });
const manga: Map<string, Manga> = new Map() const manga: Map<string, Manga> = new Map();
const promises: Map<string, Promise<Manga | undefined>> = new Map() const promises: Map<string, Promise<Manga | undefined>> = new Map();
export default function MangaProvider({ children }: { children: ReactNode }) { export default function MangaProvider({ children }: { children: ReactNode }) {
const Api = useContext(ApiContext) const Api = useContext(ApiContext);
return ( return (
<MangaContext value={{ GetManga: (k) => getManga(k, Api) }}> <MangaContext value={{ GetManga: (k) => getManga(k, Api) }}>
{children} {children}
</MangaContext> </MangaContext>
) );
} }
function getManga(key: string, Api: V2): Promise<Manga | undefined> { function getManga(key: string, Api: V2): Promise<Manga | undefined> {
if (manga.has(key)) return Promise.resolve(manga.get(key)) if (manga.has(key)) return Promise.resolve(manga.get(key));
if (promises.has(key)) return promises.get(key)! if (promises.has(key)) return promises.get(key)!;
const newPromise = Api.mangaDetail(key) const newPromise = Api.mangaDetail(key)
.then((data) => { .then((data) => {
if (data.ok) { if (data.ok) {
manga.set(key, data.data) manga.set(key, data.data);
return data.data return data.data;
} else return undefined } else return undefined;
}) })
.catch(() => { .catch(() => {
return undefined return undefined;
}) });
promises.set(key, newPromise) promises.set(key, newPromise);
return newPromise return newPromise;
} }
export interface M { export interface M {
GetManga(key: string): Promise<Manga | undefined> GetManga(key: string): Promise<Manga | undefined>;
} }

View File

@@ -1,12 +1,12 @@
import { createRoot } from 'react-dom/client' import { createRoot } from 'react-dom/client';
import './index.css' import './index.css';
import App from './App.tsx' import App from './App.tsx';
// @ts-expect-error font // @ts-expect-error font
import '@fontsource/inter' 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' import { trangaTheme } from './theme.ts';
export default function MyApp() { export default function MyApp() {
return ( return (
@@ -19,7 +19,7 @@ export default function MyApp() {
<App /> <App />
</CssVarsProvider> </CssVarsProvider>
</StrictMode> </StrictMode>
) );
} }
createRoot(document.getElementById('root')!).render(<MyApp />) createRoot(document.getElementById('root')!).render(<MyApp />);

View File

@@ -1,4 +1,4 @@
import { extendTheme } from '@mui/joy/styles' import { extendTheme } from '@mui/joy/styles';
export const trangaTheme = extendTheme({ export const trangaTheme = extendTheme({
colorSchemes: { colorSchemes: {
@@ -80,8 +80,6 @@ export const trangaTheme = extendTheme({
}, },
}, },
}, },
dark: { dark: { palette: {} },
palette: {},
},
}, },
}) });

View File

@@ -1,10 +1,8 @@
import { defineConfig } from 'vite' import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react' import react from '@vitejs/plugin-react';
// https://vite.dev/config/ // https://vite.dev/config/
export default defineConfig({ export default defineConfig({
plugins: [react()], plugins: [react()],
server: { server: { host: '127.0.0.1' },
host: '127.0.0.1', });
},
})