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",
"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,
},
},
})
});
```
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
// eslint.config.js
import reactX from 'eslint-plugin-react-x'
import reactDom from 'eslint-plugin-react-dom'
import reactX from 'eslint-plugin-react-x';
import reactDom from 'eslint-plugin-react-dom';
export default tseslint.config({
plugins: {
@@ -50,5 +50,5 @@ export default tseslint.config({
...reactX.configs['recommended-typescript'].rules,
...reactDom.configs.recommended.rules,
},
})
});
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,5 +1,5 @@
import { Manga } from './api/data-contracts.ts'
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react'
import { Manga } from './api/data-contracts.ts';
import { Dispatch, ReactNode, useContext, useEffect, useState } from 'react';
import {
Card,
CardCover,
@@ -9,30 +9,32 @@ import {
Stack,
Typography,
useTheme,
} from '@mui/joy'
import ModalClose from '@mui/joy/ModalClose'
import { ApiContext } from './contexts/ApiContext.tsx'
import { MangaContext } from './contexts/MangaContext.tsx'
import './Components/Mangas/MangaCard.css'
import MarkdownPreview from '@uiw/react-markdown-preview'
} from '@mui/joy';
import ModalClose from '@mui/joy/ModalClose';
import { ApiContext } from './contexts/ApiContext.tsx';
import { MangaContext } from './contexts/MangaContext.tsx';
import './Components/Mangas/MangaCard.css';
import MarkdownPreview from '@uiw/react-markdown-preview';
export default function MangaDetail(props: MangaDetailProps): ReactNode {
const Api = useContext(ApiContext)
const Manga = useContext(MangaContext)
const Api = useContext(ApiContext);
const Manga = useContext(MangaContext);
const [manga, setManga] = useState<Manga | undefined>(props.manga)
const [manga, setManga] = useState<Manga | undefined>(props.manga);
useEffect(() => {
if (!props.open) return
if (!props.mangaKey) return
if (props.manga != undefined) return
Manga.GetManga(props.mangaKey).then(setManga)
}, [Api, Manga, props])
if (!props.open) return;
if (!props.mangaKey) return;
if (props.manga != undefined) return;
Manga.GetManga(props.mangaKey).then(setManga);
}, [Api, Manga, props]);
const theme = useTheme()
const theme = useTheme();
return (
<Modal open={props.open} onClose={() => props.setOpen(false)}>
<Modal
open={props.open}
onClose={() => props.setOpen(false)}>
<ModalDialog>
<ModalClose />
<div
@@ -40,9 +42,10 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
display: 'flex',
flexWrap: 'wrap',
flexDirection: 'row',
}}
>
<Typography level={'h3'} sx={{ width: '100%' }}>
}}>
<Typography
level={'h3'}
sx={{ width: '100%' }}>
{manga?.name}
</Typography>
<Card className={'manga-card'}>
@@ -59,9 +62,11 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
<Stack
direction={'column'}
gap={2}
sx={{ maxWidth: 'calc(100% - 230px)', margin: '5px' }}
>
<Stack direction={'row'} gap={0.5} flexWrap={'wrap'}>
sx={{ maxWidth: 'calc(100% - 230px)', margin: '5px' }}>
<Stack
direction={'row'}
gap={0.5}
flexWrap={'wrap'}>
{manga?.tags.map((tag) => (
<Chip
key={tag}
@@ -69,8 +74,7 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
sx={{
backgroundColor:
theme.palette.primary.plainColor,
}}
>
}}>
{tag}
</Chip>
))}
@@ -81,8 +85,7 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
sx={{
backgroundColor:
theme.palette.success.plainColor,
}}
>
}}>
{author.name}
</Chip>
))}
@@ -93,8 +96,7 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
sx={{
backgroundColor:
theme.palette.neutral.plainColor,
}}
>
}}>
<a href={link.url}>{link.provider}</a>
</Chip>
))}
@@ -116,20 +118,19 @@ export default function MangaDetail(props: MangaDetailProps): ReactNode {
alignItems: 'flex-end',
}}
flexWrap={'nowrap'}
gap={1}
>
gap={1}>
{props.actions}
</Stack>
</div>
</ModalDialog>
</Modal>
)
);
}
export interface MangaDetailProps {
manga?: Manga
mangaKey?: string
open: boolean
setOpen: Dispatch<boolean>
actions?: ReactNode[]
manga?: Manga;
mangaKey?: string;
open: boolean;
setOpen: Dispatch<boolean>;
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 {
Box,
Card,
@@ -10,56 +10,56 @@ import {
Select,
Stack,
Typography,
} from '@mui/joy'
import ModalClose from '@mui/joy/ModalClose'
import { FileLibrary, Manga, MangaConnectorId } from './api/data-contracts.ts'
import { ApiContext } from './contexts/ApiContext.tsx'
import { MangaContext } from './contexts/MangaContext.tsx'
import { FileLibraryContext } from './contexts/FileLibraryContext.tsx'
import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx'
import TButton from './Components/Inputs/TButton.tsx'
} from '@mui/joy';
import ModalClose from '@mui/joy/ModalClose';
import { FileLibrary, Manga, MangaConnectorId } from './api/data-contracts.ts';
import { ApiContext } from './contexts/ApiContext.tsx';
import { MangaContext } from './contexts/MangaContext.tsx';
import { FileLibraryContext } from './contexts/FileLibraryContext.tsx';
import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx';
import TButton from './Components/Inputs/TButton.tsx';
export default function MangaDownloadDrawer(
props: MangaDownloadDrawerProps
): ReactNode {
const Api = useContext(ApiContext)
const Manga = useContext(MangaContext)
const Libraries = useContext(FileLibraryContext)
const Api = useContext(ApiContext);
const Manga = useContext(MangaContext);
const Libraries = useContext(FileLibraryContext);
const [manga, setManga] = useState<Manga | undefined>(props.manga)
const [library, setLibrary] = useState<FileLibrary | undefined>()
const [manga, setManga] = useState<Manga | undefined>(props.manga);
const [library, setLibrary] = useState<FileLibrary | undefined>();
const [downloadFromMap, setDownloadFromMap] = useState<
Map<MangaConnectorId, boolean>
>(new Map())
>(new Map());
useEffect(() => {
if (!props.open) return
if (!props.mangaKey) return
if (props.manga != undefined) return
Manga.GetManga(props.mangaKey).then(setManga)
}, [Api, Manga, props])
if (!props.open) return;
if (!props.mangaKey) return;
if (props.manga != undefined) return;
Manga.GetManga(props.mangaKey).then(setManga);
}, [Api, Manga, props]);
useEffect(() => {
const newMap = new Map()
const newMap = new Map();
setLibrary(
Libraries.find((library) => library.key == manga?.fileLibraryId)
)
);
manga?.mangaConnectorIds.forEach((id) => {
newMap.set(id, id.useForDownload)
})
setDownloadFromMap(newMap)
}, [manga])
newMap.set(id, id.useForDownload);
});
setDownloadFromMap(newMap);
}, [manga]);
const setDownload = async (): Promise<void> => {
if (!manga) return Promise.reject()
if (!manga) return Promise.reject();
if (library) {
const s = await Api.mangaChangeLibraryCreate(
manga.key,
library?.key
)
.then((result) => result.ok)
.catch(() => false)
if (!s) return Promise.reject()
.catch(() => false);
if (!s) return Promise.reject();
}
for (const kv of downloadFromMap) {
const s = await Api.mangaSetAsDownloadFromCreate(
@@ -68,28 +68,30 @@ export default function MangaDownloadDrawer(
kv[1]
)
.then((result) => result.ok)
.catch(() => false)
if (!s) return Promise.reject()
}
return Promise.resolve()
.catch(() => false);
if (!s) return Promise.reject();
}
return Promise.resolve();
};
const onLibraryChange = (_: any, value: string | null) => {
setLibrary(Libraries.find((library) => library.key == value))
}
setLibrary(Libraries.find((library) => library.key == value));
};
return (
<Drawer
open={props.open}
onClose={() => props.setOpen(false)}
anchor="left"
size="md"
>
size="md">
<Card sx={{ flexGrow: 1, margin: '10px' }}>
<ModalClose />
<Typography level={'h3'}>Download</Typography>
<Typography level={'h4'}>{manga?.name}</Typography>
<Stack direction={'column'} gap={2} sx={{ flexBasis: 0 }}>
<Stack
direction={'column'}
gap={2}
sx={{ flexBasis: 0 }}>
<Box>
<Typography>
Select a Library to Download to:
@@ -97,10 +99,11 @@ export default function MangaDownloadDrawer(
<Select
placeholder={'Select a Library'}
value={library?.key}
onChange={onLibraryChange}
>
onChange={onLibraryChange}>
{Libraries.map((l) => (
<Option key={l.key} value={l.key}>
<Option
key={l.key}
value={l.key}>
{l.libraryName} ({l.basePath})
</Option>
))}
@@ -128,8 +131,7 @@ export default function MangaDownloadDrawer(
display: 'flex',
alignItems: 'center',
gap: 5,
}}
>
}}>
<MangaConnectorIcon
mangaConnectorName={
id.mangaConnectorName
@@ -151,12 +153,12 @@ export default function MangaDownloadDrawer(
</Stack>
</Card>
</Drawer>
)
);
}
export interface MangaDownloadDrawerProps {
manga?: Manga
mangaKey?: string
open: boolean
setOpen: Dispatch<boolean>
manga?: Manga;
mangaKey?: string;
open: 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 {
Button,
List,
@@ -10,89 +10,91 @@ import {
StepIndicator,
Stepper,
Typography,
} from '@mui/joy'
import ModalClose from '@mui/joy/ModalClose'
import { MangaConnectorContext } from './contexts/MangaConnectorContext.tsx'
import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx'
import TInput from './Components/Inputs/TInput.tsx'
import { ApiContext } from './contexts/ApiContext.tsx'
import { MangaCardList } from './Components/Mangas/MangaList.tsx'
import { MangaConnector, MinimalManga } from './api/data-contracts.ts'
import MangaDetail from './MangaDetail.tsx'
import MangaDownloadDrawer from './MangaDownloadDrawer.tsx'
} from '@mui/joy';
import ModalClose from '@mui/joy/ModalClose';
import { MangaConnectorContext } from './contexts/MangaConnectorContext.tsx';
import MangaConnectorIcon from './Components/Mangas/MangaConnectorIcon.tsx';
import TInput from './Components/Inputs/TInput.tsx';
import { ApiContext } from './contexts/ApiContext.tsx';
import { MangaCardList } from './Components/Mangas/MangaList.tsx';
import { MangaConnector, MinimalManga } from './api/data-contracts.ts';
import MangaDetail from './MangaDetail.tsx';
import MangaDownloadDrawer from './MangaDownloadDrawer.tsx';
export function Search(props: SearchModalProps): ReactNode {
const Api = useContext(ApiContext)
const MangaConnectors = useContext(MangaConnectorContext)
const Api = useContext(ApiContext);
const MangaConnectors = useContext(MangaConnectorContext);
useEffect(() => {
if (props.open) {
setSelectedConnector(undefined)
setSearchResults([])
setSelectedConnector(undefined);
setSearchResults([]);
}
}, [props])
}, [props]);
const [selectedConnector, setSelectedConnector] = useState<MangaConnector>()
const [searchResults, setSearchResults] = useState<MinimalManga[]>([])
const [selectedConnector, setSelectedConnector] =
useState<MangaConnector>();
const [searchResults, setSearchResults] = useState<MinimalManga[]>([]);
const startSearch = async (
value: string | number | readonly string[] | undefined
): Promise<void> => {
if (typeof value != 'string') return Promise.reject()
setSearchResults([])
if (typeof value != 'string') return Promise.reject();
setSearchResults([]);
if (isUrl(value)) {
try {
const result = await Api.searchUrlCreate(value)
const result = await Api.searchUrlCreate(value);
if (result.ok) {
setSearchResults([result.data])
return Promise.resolve()
} else return Promise.reject()
setSearchResults([result.data]);
return Promise.resolve();
} else return Promise.reject();
} catch (reason) {
return await Promise.reject(reason)
return await Promise.reject(reason);
}
} else {
if (!selectedConnector) return Promise.reject()
if (!selectedConnector) return Promise.reject();
try {
const result2 = await Api.searchDetail(
selectedConnector?.key,
value
)
);
if (result2.ok) {
setSearchResults(result2.data)
return Promise.resolve()
} else return Promise.reject()
setSearchResults(result2.data);
return Promise.resolve();
} else return Promise.reject();
} catch (reason1) {
return await Promise.reject(reason1)
}
return await Promise.reject(reason1);
}
}
};
const [selectedManga, setSelectedManga] = useState<
MinimalManga | undefined
>(undefined)
const [mangaDetailOpen, setMangaDetailOpen] = useState(false)
>(undefined);
const [mangaDetailOpen, setMangaDetailOpen] = useState(false);
const [mangaDownloadDrawerOpen, setMangaDownloadDrawerOpen] =
useState(false)
useState(false);
function openMangaDetail(manga: MinimalManga) {
setSelectedManga(manga)
setMangaDetailOpen(true)
setSelectedManga(manga);
setMangaDetailOpen(true);
}
function openMangaDownloadDrawer() {
setMangaDetailOpen(false)
setMangaDownloadDrawerOpen(true)
setMangaDetailOpen(false);
setMangaDownloadDrawerOpen(true);
}
return (
<Modal open={props.open} onClose={() => props.setOpen(false)}>
<Modal
open={props.open}
onClose={() => props.setOpen(false)}>
<ModalDialog sx={{ width: '90vw' }}>
<ModalClose />
<Stepper>
<Step
orientation={'vertical'}
indicator={<StepIndicator>1</StepIndicator>}
>
indicator={<StepIndicator>1</StepIndicator>}>
<Typography level={'title-lg'}>
Select a connector
</Typography>
@@ -100,8 +102,7 @@ export function Search(props: SearchModalProps): ReactNode {
{MangaConnectors.map((c) => (
<ListItem
key={c.key}
onClick={() => setSelectedConnector(c)}
>
onClick={() => setSelectedConnector(c)}>
<ListItemDecorator>
<MangaConnectorIcon
mangaConnector={c}
@@ -112,8 +113,7 @@ export function Search(props: SearchModalProps): ReactNode {
c.key == selectedConnector?.key
? { fontWeight: 'bold' }
: {}
}
>
}>
{c.name}
</Typography>
</ListItem>
@@ -122,8 +122,7 @@ export function Search(props: SearchModalProps): ReactNode {
</Step>
<Step
orientation={'vertical'}
indicator={<StepIndicator>2</StepIndicator>}
>
indicator={<StepIndicator>2</StepIndicator>}>
<Typography level={'title-lg'}>
Enter a search term or URL
</Typography>
@@ -154,19 +153,19 @@ export function Search(props: SearchModalProps): ReactNode {
/>
</ModalDialog>
</Modal>
)
);
}
export interface SearchModalProps {
open: boolean
setOpen: Dispatch<boolean>
open: boolean;
setOpen: Dispatch<boolean>;
}
function isUrl(str: string): boolean {
try {
new URL(str)
return true
new URL(str);
return true;
} catch {
return false
return false;
}
}

View File

@@ -30,8 +30,8 @@ import {
TrangaSettings,
Worker,
WorkerExecutionState,
} from './data-contracts'
import { ContentType, HttpClient, RequestParams } from './http-client'
} from './data-contracts';
import { ContentType, HttpClient, RequestParams } from './http-client';
export class V2<
SecurityDataType = unknown,
@@ -50,7 +50,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -66,7 +66,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -81,7 +81,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -95,7 +95,7 @@ export class V2<
path: `/v2/FileLibrary/${fileLibraryId}`,
method: 'DELETE',
...params,
})
});
/**
* No description
*
@@ -115,7 +115,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -135,7 +135,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -150,7 +150,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -169,7 +169,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -187,7 +187,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -204,7 +204,7 @@ export class V2<
path: `/v2/LibraryConnector/${libraryConnectorId}`,
method: 'DELETE',
...params,
})
});
/**
* No description
*
@@ -218,7 +218,7 @@ export class V2<
path: `/v2/Maintenance/CleanupNoDownloadManga`,
method: 'POST',
...params,
})
});
/**
* No description
*
@@ -233,7 +233,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -248,7 +248,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -263,7 +263,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -280,7 +280,7 @@ export class V2<
type: ContentType.Json,
format: 'json',
...params,
})
});
/**
* No description
*
@@ -295,7 +295,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -309,7 +309,7 @@ export class V2<
path: `/v2/Manga/${mangaId}`,
method: 'DELETE',
...params,
})
});
/**
* No description
*
@@ -327,7 +327,7 @@ export class V2<
path: `/v2/Manga/${mangaIdFrom}/MergeInto/${mangaIdInto}`,
method: 'PATCH',
...params,
})
});
/**
* No description
*
@@ -343,12 +343,12 @@ export class V2<
* If width is provided, height needs to also be provided
* @format int32
*/
width?: number
width?: number;
/**
* If height is provided, width needs to also be provided
* @format int32
*/
height?: number
height?: number;
},
params: RequestParams = {}
) =>
@@ -358,7 +358,7 @@ export class V2<
query: query,
format: 'blob',
...params,
})
});
/**
* No description
*
@@ -373,7 +373,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -391,7 +391,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -409,7 +409,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -427,7 +427,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -445,7 +445,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -465,7 +465,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -483,7 +483,7 @@ export class V2<
path: `/v2/Manga/${mangaId}/ChangeLibrary/${libraryId}`,
method: 'POST',
...params,
})
});
/**
* No description
*
@@ -502,7 +502,7 @@ export class V2<
path: `/v2/Manga/${mangaId}/SetAsDownloadFrom/${mangaConnectorName}/${isRequested}`,
method: 'POST',
...params,
})
});
/**
* No description
*
@@ -521,7 +521,7 @@ export class V2<
method: 'POST',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -536,7 +536,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -551,7 +551,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -566,7 +566,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -584,7 +584,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -599,7 +599,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -614,7 +614,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -632,7 +632,7 @@ export class V2<
path: `/v2/MangaConnector/${mangaConnectorName}/SetEnabled/${enabled}`,
method: 'PATCH',
...params,
})
});
/**
* No description
*
@@ -647,7 +647,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -662,7 +662,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -684,7 +684,7 @@ export class V2<
type: ContentType.Json,
format: 'json',
...params,
})
});
/**
* No description
*
@@ -706,7 +706,7 @@ export class V2<
type: ContentType.Json,
format: 'json',
...params,
})
});
/**
* No description
*
@@ -724,7 +724,7 @@ export class V2<
path: `/v2/MetadataFetcher/${metadataFetcherName}/Unlink/${mangaId}`,
method: 'POST',
...params,
})
});
/**
* No description
*
@@ -739,7 +739,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* @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,
format: 'json',
...params,
})
});
/**
* No description
*
@@ -774,7 +774,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -788,7 +788,7 @@ export class V2<
path: `/v2/NotificationConnector/${name}`,
method: 'DELETE',
...params,
})
});
/**
* @description Priority needs to be between 0 and 10
*
@@ -808,7 +808,7 @@ export class V2<
type: ContentType.Json,
format: 'json',
...params,
})
});
/**
* @description Priority needs to be between 1 and 5
*
@@ -828,7 +828,7 @@ export class V2<
type: ContentType.Json,
format: 'json',
...params,
})
});
/**
* @description https://pushover.net/api
*
@@ -848,7 +848,7 @@ export class V2<
type: ContentType.Json,
format: 'json',
...params,
})
});
/**
* No description
*
@@ -863,7 +863,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -878,7 +878,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -896,7 +896,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -911,7 +911,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -929,7 +929,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -948,7 +948,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -965,7 +965,7 @@ export class V2<
type: ContentType.Json,
format: 'json',
...params,
})
});
/**
* No description
*
@@ -980,7 +980,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -994,7 +994,7 @@ export class V2<
path: `/v2/Settings/UserAgent`,
method: 'GET',
...params,
})
});
/**
* No description
*
@@ -1013,7 +1013,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -1027,7 +1027,7 @@ export class V2<
path: `/v2/Settings/UserAgent`,
method: 'DELETE',
...params,
})
});
/**
* No description
*
@@ -1040,17 +1040,17 @@ export class V2<
this.request<
{
/** @format int32 */
Default?: number
Default?: number;
/** @format int32 */
MangaDexFeed?: number
MangaDexFeed?: number;
/** @format int32 */
MangaImage?: number
MangaImage?: number;
/** @format int32 */
MangaCover?: number
MangaCover?: number;
/** @format int32 */
MangaDexImage?: number
MangaDexImage?: number;
/** @format int32 */
MangaInfo?: number
MangaInfo?: number;
},
any
>({
@@ -1058,7 +1058,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* @description <h1>NOT IMPLEMENTED</h1>
*
@@ -1072,7 +1072,7 @@ export class V2<
path: `/v2/Settings/RequestLimits`,
method: 'PATCH',
...params,
})
});
/**
* No description
*
@@ -1087,7 +1087,7 @@ export class V2<
method: 'DELETE',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -1109,7 +1109,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -1129,7 +1129,7 @@ export class V2<
method: 'DELETE',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -1143,7 +1143,7 @@ export class V2<
path: `/v2/Settings/ImageCompressionLevel`,
method: 'GET',
...params,
})
});
/**
* No description
*
@@ -1160,7 +1160,7 @@ export class V2<
path: `/v2/Settings/ImageCompressionLevel/${level}`,
method: 'PATCH',
...params,
})
});
/**
* No description
*
@@ -1174,7 +1174,7 @@ export class V2<
path: `/v2/Settings/BWImages`,
method: 'GET',
...params,
})
});
/**
* No description
*
@@ -1191,7 +1191,7 @@ export class V2<
path: `/v2/Settings/BWImages/${enabled}`,
method: 'PATCH',
...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
*
@@ -1205,7 +1205,7 @@ export class V2<
path: `/v2/Settings/ChapterNamingScheme`,
method: 'GET',
...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
*
@@ -1224,7 +1224,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -1243,7 +1243,7 @@ export class V2<
body: data,
type: ContentType.Json,
...params,
})
});
/**
* No description
*
@@ -1257,7 +1257,7 @@ export class V2<
path: `/v2/Settings/FlareSolverr/Url`,
method: 'DELETE',
...params,
})
});
/**
* No description
*
@@ -1271,7 +1271,7 @@ export class V2<
path: `/v2/Settings/FlareSolverr/Test`,
method: 'POST',
...params,
})
});
/**
* No description
*
@@ -1285,7 +1285,7 @@ export class V2<
path: `/v2/Settings/DownloadLanguage`,
method: 'GET',
...params,
})
});
/**
* No description
*
@@ -1302,7 +1302,7 @@ export class V2<
path: `/v2/Settings/DownloadLanguage/${language}`,
method: 'PATCH',
...params,
})
});
/**
* No description
*
@@ -1317,7 +1317,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -1332,7 +1332,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -1350,7 +1350,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -1365,7 +1365,7 @@ export class V2<
method: 'GET',
format: 'json',
...params,
})
});
/**
* No description
*
@@ -1379,7 +1379,7 @@ export class V2<
path: `/v2/Worker/${workerId}`,
method: 'DELETE',
...params,
})
});
/**
* No description
*
@@ -1393,7 +1393,7 @@ export class V2<
path: `/v2/Worker/${workerId}/Start`,
method: 'POST',
...params,
})
});
/**
* No description
*
@@ -1407,5 +1407,5 @@ export class V2<
path: `/v2/Worker/${workerId}/Stop`,
method: 'POST',
...params,
})
});
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,43 +1,43 @@
import { createContext, ReactNode, useContext } from 'react'
import { ApiContext } from './ApiContext.tsx'
import { Manga } from '../api/data-contracts.ts'
import { V2 } from '../api/V2.ts'
import { createContext, ReactNode, useContext } from 'react';
import { ApiContext } from './ApiContext.tsx';
import { Manga } from '../api/data-contracts.ts';
import { V2 } from '../api/V2.ts';
export const MangaContext = createContext<M>({
GetManga: () => Promise.reject(),
})
const manga: Map<string, Manga> = new Map()
const promises: Map<string, Promise<Manga | undefined>> = new Map()
});
const manga: Map<string, Manga> = new Map();
const promises: Map<string, Promise<Manga | undefined>> = new Map();
export default function MangaProvider({ children }: { children: ReactNode }) {
const Api = useContext(ApiContext)
const Api = useContext(ApiContext);
return (
<MangaContext value={{ GetManga: (k) => getManga(k, Api) }}>
{children}
</MangaContext>
)
);
}
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)
.then((data) => {
if (data.ok) {
manga.set(key, data.data)
return data.data
} else return undefined
manga.set(key, data.data);
return data.data;
} else return undefined;
})
.catch(() => {
return undefined
})
promises.set(key, newPromise)
return newPromise
return undefined;
});
promises.set(key, newPromise);
return newPromise;
}
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 './index.css'
import App from './App.tsx'
import { createRoot } from 'react-dom/client';
import './index.css';
import App from './App.tsx';
// @ts-expect-error font
import '@fontsource/inter'
import { CssVarsProvider } from '@mui/joy/styles'
import CssBaseline from '@mui/joy/CssBaseline'
import { StrictMode } from 'react'
import { trangaTheme } from './theme.ts'
import '@fontsource/inter';
import { CssVarsProvider } from '@mui/joy/styles';
import CssBaseline from '@mui/joy/CssBaseline';
import { StrictMode } from 'react';
import { trangaTheme } from './theme.ts';
export default function MyApp() {
return (
@@ -19,7 +19,7 @@ export default function MyApp() {
<App />
</CssVarsProvider>
</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({
colorSchemes: {
@@ -80,8 +80,6 @@ export const trangaTheme = extendTheme({
},
},
},
dark: {
palette: {},
dark: { palette: {} },
},
},
})
});

View File

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