mirror of
https://github.com/C9Glax/tranga-website.git
synced 2025-09-10 11:58:20 +02:00
Rebuild with custom components
This commit is contained in:
6
tranga-website/.prettierrc
Normal file
6
tranga-website/.prettierrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"trailingComma": "es5",
|
||||||
|
"tabWidth": 4,
|
||||||
|
"semi": false,
|
||||||
|
"singleQuote": true
|
||||||
|
}
|
@@ -13,42 +13,42 @@ If you are developing a production application, we recommend updating the config
|
|||||||
|
|
||||||
```js
|
```js
|
||||||
export default tseslint.config({
|
export default tseslint.config({
|
||||||
extends: [
|
extends: [
|
||||||
// Remove ...tseslint.configs.recommended and replace with this
|
// Remove ...tseslint.configs.recommended and replace with this
|
||||||
...tseslint.configs.recommendedTypeChecked,
|
...tseslint.configs.recommendedTypeChecked,
|
||||||
// Alternatively, use this for stricter rules
|
// Alternatively, use this for stricter rules
|
||||||
...tseslint.configs.strictTypeChecked,
|
...tseslint.configs.strictTypeChecked,
|
||||||
// Optionally, add this for stylistic rules
|
// Optionally, add this for stylistic rules
|
||||||
...tseslint.configs.stylisticTypeChecked,
|
...tseslint.configs.stylisticTypeChecked,
|
||||||
],
|
],
|
||||||
languageOptions: {
|
languageOptions: {
|
||||||
// other options...
|
// other options...
|
||||||
parserOptions: {
|
parserOptions: {
|
||||||
project: ["./tsconfig.node.json", "./tsconfig.app.json"],
|
project: ['./tsconfig.node.json', './tsconfig.app.json'],
|
||||||
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: {
|
||||||
// Add the react-x and react-dom plugins
|
// Add the react-x and react-dom plugins
|
||||||
"react-x": reactX,
|
'react-x': reactX,
|
||||||
"react-dom": reactDom,
|
'react-dom': reactDom,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
// other rules...
|
// other rules...
|
||||||
// Enable its recommended typescript rules
|
// Enable its recommended typescript rules
|
||||||
...reactX.configs["recommended-typescript"].rules,
|
...reactX.configs['recommended-typescript'].rules,
|
||||||
...reactDom.configs.recommended.rules,
|
...reactDom.configs.recommended.rules,
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
```
|
```
|
||||||
|
@@ -1,28 +1,28 @@
|
|||||||
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,
|
ecmaVersion: 2020,
|
||||||
globals: globals.browser,
|
globals: globals.browser,
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
"react-hooks": reactHooks,
|
'react-hooks': reactHooks,
|
||||||
"react-refresh": reactRefresh,
|
'react-refresh': reactRefresh,
|
||||||
},
|
},
|
||||||
rules: {
|
rules: {
|
||||||
...reactHooks.configs.recommended.rules,
|
...reactHooks.configs.recommended.rules,
|
||||||
"react-refresh/only-export-components": [
|
'react-refresh/only-export-components': [
|
||||||
"warn",
|
'warn',
|
||||||
{ allowConstantExport: true },
|
{ allowConstantExport: true },
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
);
|
)
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<!doctype html>
|
<!doctype html>
|
||||||
<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 rel="icon" type="image/png" href="/blahaj.png" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<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>
|
||||||
|
14420
tranga-website/package-lock.json
generated
14420
tranga-website/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,39 +1,39 @@
|
|||||||
{
|
{
|
||||||
"name": "tranga-website",
|
"name": "tranga-website",
|
||||||
"private": true,
|
"private": true,
|
||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "tsc -b && vite build",
|
"build": "tsc -b && vite build",
|
||||||
"lint": "eslint . --fix",
|
"lint": "eslint . --fix",
|
||||||
"swagger-api": "swagger-typescript-api generate -p http://127.0.0.1:6531/swagger/v2/swagger.json -o ./src/apiClient --modular && prettier ./src/apiClient --write",
|
"swagger-api": "swagger-typescript-api generate -p http://127.0.0.1:6531/swagger/v2/swagger.json -o ./src/api --modular && prettier ./src/api --write",
|
||||||
"prettier:check": "prettier . --check",
|
"prettier:check": "prettier . --check",
|
||||||
"prettier": "prettier . --write"
|
"prettier": "prettier . --write"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@emotion/react": "^11.14.0",
|
"@emotion/react": "^11.14.0",
|
||||||
"@emotion/styled": "^11.14.0",
|
"@emotion/styled": "^11.14.0",
|
||||||
"@fontsource/inter": "^5.2.5",
|
"@fontsource/inter": "^5.2.5",
|
||||||
"@mui/icons-material": "^7.0.1",
|
"@mui/icons-material": "^7.0.1",
|
||||||
"@mui/joy": "^5.0.0-beta.52",
|
"@mui/joy": "^5.0.0-beta.52",
|
||||||
"@uiw/react-markdown-preview": "^5.1.4",
|
"@uiw/react-markdown-preview": "^5.1.4",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
"react-dom": "^19.0.0"
|
"react-dom": "^19.0.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/js": "^9.21.0",
|
"@eslint/js": "^9.21.0",
|
||||||
"@types/react": "^19.0.10",
|
"@types/react": "^19.0.10",
|
||||||
"@types/react-dom": "^19.0.4",
|
"@types/react-dom": "^19.0.4",
|
||||||
"@vitejs/plugin-react": "^4.3.4",
|
"@vitejs/plugin-react": "^4.3.4",
|
||||||
"eslint": "^9.21.0",
|
"eslint": "^9.21.0",
|
||||||
"eslint-plugin-react-hooks": "^5.1.0",
|
"eslint-plugin-react-hooks": "^5.1.0",
|
||||||
"eslint-plugin-react-refresh": "^0.4.19",
|
"eslint-plugin-react-refresh": "^0.4.19",
|
||||||
"globals": "^15.15.0",
|
"globals": "^15.15.0",
|
||||||
"prettier": "3.6.2",
|
"prettier": "3.6.2",
|
||||||
"swagger-typescript-api": "^13.2.7",
|
"swagger-typescript-api": "^13.2.7",
|
||||||
"typescript": "~5.7.2",
|
"typescript": "~5.7.2",
|
||||||
"typescript-eslint": "^8.24.1",
|
"typescript-eslint": "^8.24.1",
|
||||||
"vite": "^6.2.0"
|
"vite": "^6.2.0"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
.app {
|
.app {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
top: 0;
|
top: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.app-content {
|
.app-content {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
left: 0;
|
left: 0;
|
||||||
}
|
}
|
||||||
|
@@ -1,108 +1,44 @@
|
|||||||
import Sheet from "@mui/joy/Sheet";
|
import Sheet from '@mui/joy/Sheet'
|
||||||
import "./App.css";
|
import Header from './Header.tsx'
|
||||||
import Settings from "./Components/Settings/Settings.tsx";
|
import Settings from './Components/Settings/Settings.tsx'
|
||||||
import Header from "./Header.tsx";
|
import ApiProvider from './contexts/ApiContext.tsx'
|
||||||
import { createContext, ReactNode, useEffect, useState } from "react";
|
import { useEffect, useState } from 'react'
|
||||||
import { V2 } from "./apiClient/V2.ts";
|
import { ApiConfig } from './api/http-client.ts'
|
||||||
import { ApiContext } from "./apiClient/ApiContext.tsx";
|
import MangaProvider from './contexts/MangaContext.tsx'
|
||||||
import MangaList from "./Components/Mangas/MangaList.tsx";
|
import MangaList from './Components/Mangas/MangaList.tsx'
|
||||||
import {
|
import Search from './Search.tsx'
|
||||||
FileLibrary,
|
import MangaConnectorProvider from './contexts/MangaConnectorContext.tsx'
|
||||||
Manga,
|
|
||||||
MangaConnector,
|
|
||||||
MinimalManga,
|
|
||||||
} from "./apiClient/data-contracts.ts";
|
|
||||||
import Search from "./Components/Search.tsx";
|
|
||||||
import { Typography } from "@mui/joy";
|
|
||||||
import Workers from "./Components/WorkerModal/Workers.tsx";
|
|
||||||
|
|
||||||
const apiUri =
|
|
||||||
localStorage.getItem("apiUri") ??
|
|
||||||
window.location.href.substring(0, window.location.href.lastIndexOf("/")) +
|
|
||||||
"/api";
|
|
||||||
localStorage.setItem("apiUri", apiUri);
|
|
||||||
const Api = new V2({ baseUrl: apiUri });
|
|
||||||
|
|
||||||
const manga: Manga[] = [];
|
|
||||||
const promises: Map<string, Promise<Manga | undefined>> = new Map();
|
|
||||||
const getManga = async (key: string): Promise<Manga | undefined> => {
|
|
||||||
const result = manga.find((m) => m.key === key);
|
|
||||||
if (result) return result;
|
|
||||||
if (promises.has(key)) return promises.get(key);
|
|
||||||
const newPromise = retrieveManga(key);
|
|
||||||
promises.set(key, newPromise);
|
|
||||||
return newPromise;
|
|
||||||
};
|
|
||||||
|
|
||||||
const retrieveManga = async (key: string): Promise<Manga | undefined> => {
|
|
||||||
return Api.mangaDetail(key).then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
manga.push(response.data);
|
|
||||||
return response.data;
|
|
||||||
}
|
|
||||||
return undefined;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
export const MangaConnectorContext = createContext<MangaConnector[]>([]);
|
|
||||||
export const MangaContext = createContext<{
|
|
||||||
getManga: (key: string) => Promise<Manga | undefined>;
|
|
||||||
}>({
|
|
||||||
getManga,
|
|
||||||
});
|
|
||||||
export const FileLibraryContext = createContext<FileLibrary[]>([]);
|
|
||||||
|
|
||||||
const updateApiUri = (uri: string) => {
|
|
||||||
localStorage.setItem("apiUri", uri);
|
|
||||||
window.location.reload();
|
|
||||||
};
|
|
||||||
|
|
||||||
export default function App() {
|
export default function App() {
|
||||||
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
|
const [apiUri, setApiUri] = useState<string>(
|
||||||
const [downloadingManga, setDownloadingManga] = useState<MinimalManga[]>([]);
|
localStorage.getItem('apiUri') ??
|
||||||
const [fileLibraries, setFileLibraries] = useState<FileLibrary[]>([]);
|
window.location.href.substring(
|
||||||
|
0,
|
||||||
|
window.location.href.lastIndexOf('/')
|
||||||
|
) + '/api'
|
||||||
|
)
|
||||||
|
const [apiConfig, setApiConfig] = useState<ApiConfig>({ baseUrl: apiUri })
|
||||||
|
useEffect(() => {
|
||||||
|
setApiConfig({ baseUrl: apiUri })
|
||||||
|
}, [apiUri])
|
||||||
|
|
||||||
useEffect(() => {
|
const [searchOpen, setSearchOpen] = useState<boolean>(false)
|
||||||
Api.mangaConnectorList().then((response) => {
|
|
||||||
if (response.ok) setMangaConnectors(response.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
Api.fileLibraryList().then((response) => {
|
return (
|
||||||
if (response.ok) setFileLibraries(response.data);
|
<ApiProvider apiConfig={apiConfig}>
|
||||||
});
|
<MangaConnectorProvider>
|
||||||
|
<MangaProvider>
|
||||||
Api.mangaDownloadingList().then((response) => {
|
<Sheet className={'app'}>
|
||||||
if (response.ok) setDownloadingManga(response.data);
|
<Header>
|
||||||
});
|
<Settings setApiUri={setApiUri} />
|
||||||
}, []);
|
</Header>
|
||||||
|
<Sheet className={'app-content'}>
|
||||||
return (
|
<MangaList openSearch={() => setSearchOpen(true)} />
|
||||||
<ApiContext.Provider value={Api}>
|
<Search open={searchOpen} setOpen={setSearchOpen} />
|
||||||
<FileLibraryContext value={fileLibraries}>
|
</Sheet>
|
||||||
<MangaConnectorContext.Provider value={mangaConnectors}>
|
</Sheet>
|
||||||
<MangaContext.Provider value={{ getManga }}>
|
</MangaProvider>
|
||||||
{Api ? (
|
</MangaConnectorProvider>
|
||||||
<Sheet className={"app"}>
|
</ApiProvider>
|
||||||
<Header>
|
)
|
||||||
<Settings setApiUri={updateApiUri} />
|
|
||||||
<Workers />
|
|
||||||
</Header>
|
|
||||||
<Sheet className={"app-content"}>
|
|
||||||
<MangaList manga={downloadingManga}>
|
|
||||||
<Search />
|
|
||||||
</MangaList>
|
|
||||||
</Sheet>
|
|
||||||
</Sheet>
|
|
||||||
) : (
|
|
||||||
<Loading />
|
|
||||||
)}
|
|
||||||
</MangaContext.Provider>
|
|
||||||
</MangaConnectorContext.Provider>
|
|
||||||
</FileLibraryContext>
|
|
||||||
</ApiContext.Provider>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Loading(): ReactNode {
|
|
||||||
return <Typography>Loading</Typography>;
|
|
||||||
}
|
}
|
||||||
|
33
tranga-website/src/Components/Inputs/TButton.tsx
Normal file
33
tranga-website/src/Components/Inputs/TButton.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import { Button, CircularProgress } 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 clicked: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||||
|
setState(TState.busy)
|
||||||
|
e.preventDefault()
|
||||||
|
if (props.completionAction)
|
||||||
|
props
|
||||||
|
.completionAction(undefined)
|
||||||
|
.then(() => setState(TState.success))
|
||||||
|
.catch(() => setState(TState.failure))
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Button
|
||||||
|
color={TColor(state)}
|
||||||
|
disabled={props.disabled ?? TDisabled(state)}
|
||||||
|
aria-disabled={props.disabled ?? TDisabled(state)}
|
||||||
|
onClick={clicked}
|
||||||
|
startDecorator={TDisabled(state) ? <CircularProgress /> : null}
|
||||||
|
>
|
||||||
|
{props.children}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TButtonProps extends TProps {
|
||||||
|
children?: ReactNode
|
||||||
|
}
|
89
tranga-website/src/Components/Inputs/TInput.tsx
Normal file
89
tranga-website/src/Components/Inputs/TInput.tsx
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Button, CircularProgress, Input } from '@mui/joy'
|
||||||
|
import { MouseEventHandler, useEffect, useState } from 'react'
|
||||||
|
import * as React from 'react'
|
||||||
|
import TProps, { TColor, TDisabled, TState } from './TProps.ts'
|
||||||
|
|
||||||
|
export default function TInput(props: TInputProps) {
|
||||||
|
const [state, setState] = useState<TState>(TState.clean)
|
||||||
|
const [value, setValue] = useState<
|
||||||
|
string | number | readonly string[] | undefined
|
||||||
|
>(props.defaultValue)
|
||||||
|
const [initialValue, setInitialValue] = useState<
|
||||||
|
string | number | readonly string[] | undefined
|
||||||
|
>(props.defaultValue)
|
||||||
|
|
||||||
|
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined)
|
||||||
|
|
||||||
|
const inputValueChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
e.preventDefault()
|
||||||
|
setValue(e.target.value)
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
|
||||||
|
if (!props.autoSubmit) return
|
||||||
|
|
||||||
|
timerRef.current = setTimeout(() => {
|
||||||
|
submit()
|
||||||
|
}, props.actionDelay ?? 1500)
|
||||||
|
}
|
||||||
|
|
||||||
|
const submitClicked: MouseEventHandler<HTMLAnchorElement> = (e) => {
|
||||||
|
e.preventDefault()
|
||||||
|
submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const keyDownHandler = (e: React.KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
if (e.key === 'Enter') submit()
|
||||||
|
}
|
||||||
|
|
||||||
|
const submit = () => {
|
||||||
|
setState(TState.busy)
|
||||||
|
clearTimeout(timerRef.current)
|
||||||
|
if (props.completionAction)
|
||||||
|
props
|
||||||
|
.completionAction(value)
|
||||||
|
.then(() => {
|
||||||
|
setState(TState.success)
|
||||||
|
setInitialValue(value)
|
||||||
|
})
|
||||||
|
.catch(() => setState(TState.failure))
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (value == initialValue) {
|
||||||
|
setState(TState.clean)
|
||||||
|
}
|
||||||
|
}, [value, initialValue])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
color={TColor(state)}
|
||||||
|
disabled={props.disabled ?? TDisabled(state)}
|
||||||
|
aria-disabled={props.disabled ?? TDisabled(state)}
|
||||||
|
placeholder={props.placeholder}
|
||||||
|
value={value}
|
||||||
|
onChange={inputValueChanged}
|
||||||
|
onKeyDown={keyDownHandler}
|
||||||
|
startDecorator={TDisabled(state) ? <CircularProgress /> : null}
|
||||||
|
endDecorator={
|
||||||
|
props.submitButtonHidden ? null : (
|
||||||
|
<Button
|
||||||
|
onClick={submitClicked}
|
||||||
|
disabled={props.disabled ?? TDisabled(state)}
|
||||||
|
aria-disabled={props.disabled ?? TDisabled(state)}
|
||||||
|
>
|
||||||
|
{props.submitButtonText ?? 'Submit'}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface TInputProps extends TProps {
|
||||||
|
placeholder?: string
|
||||||
|
defaultValue?: string | number | readonly string[]
|
||||||
|
actionDelay?: number
|
||||||
|
autoSubmit?: boolean
|
||||||
|
submitButtonHidden?: boolean
|
||||||
|
submitButtonText?: string
|
||||||
|
}
|
40
tranga-website/src/Components/Inputs/TProps.ts
Normal file
40
tranga-website/src/Components/Inputs/TProps.ts
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
import { ColorPaletteProp } from '@mui/joy'
|
||||||
|
|
||||||
|
export enum TState {
|
||||||
|
clean,
|
||||||
|
dirty,
|
||||||
|
busy,
|
||||||
|
success,
|
||||||
|
failure,
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TDisabled = (state: TState): boolean => {
|
||||||
|
switch (state) {
|
||||||
|
case TState.busy:
|
||||||
|
return true
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const TColor = (state: TState): ColorPaletteProp => {
|
||||||
|
switch (state) {
|
||||||
|
case TState.clean:
|
||||||
|
return 'primary'
|
||||||
|
case TState.dirty:
|
||||||
|
return 'warning'
|
||||||
|
case TState.busy:
|
||||||
|
return 'neutral'
|
||||||
|
case TState.success:
|
||||||
|
return 'success'
|
||||||
|
case TState.failure:
|
||||||
|
return 'warning'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default interface TProps {
|
||||||
|
disabled?: boolean
|
||||||
|
completionAction?: (
|
||||||
|
value: string | number | readonly string[] | undefined
|
||||||
|
) => Promise<void>
|
||||||
|
}
|
@@ -1,34 +0,0 @@
|
|||||||
import { Close, Done } from "@mui/icons-material";
|
|
||||||
import { CircularProgress, ColorPaletteProp } from "@mui/joy";
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
|
|
||||||
export enum LoadingState {
|
|
||||||
none,
|
|
||||||
loading,
|
|
||||||
success,
|
|
||||||
failure,
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StateIndicator(state: LoadingState): ReactNode {
|
|
||||||
switch (state) {
|
|
||||||
case LoadingState.loading:
|
|
||||||
return <CircularProgress />;
|
|
||||||
case LoadingState.failure:
|
|
||||||
return <Close />;
|
|
||||||
case LoadingState.success:
|
|
||||||
return <Done />;
|
|
||||||
default:
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export function StateColor(state: LoadingState): ColorPaletteProp | undefined {
|
|
||||||
switch (state) {
|
|
||||||
case LoadingState.failure:
|
|
||||||
return "danger";
|
|
||||||
case LoadingState.success:
|
|
||||||
return "success";
|
|
||||||
default:
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,102 +0,0 @@
|
|||||||
import {
|
|
||||||
CSSProperties,
|
|
||||||
ReactNode,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import {
|
|
||||||
ChapterMangaConnectorId,
|
|
||||||
MangaConnector,
|
|
||||||
MangaMangaConnectorId,
|
|
||||||
} from "../apiClient/data-contracts.ts";
|
|
||||||
import { Link, Tooltip, Typography } from "@mui/joy";
|
|
||||||
import { MangaConnectorContext } from "../App.tsx";
|
|
||||||
import { ApiContext } from "../apiClient/ApiContext.tsx";
|
|
||||||
|
|
||||||
export default function MangaConnectorLink({
|
|
||||||
MangaConnectorId,
|
|
||||||
imageStyle,
|
|
||||||
printName,
|
|
||||||
}: {
|
|
||||||
MangaConnectorId: MangaMangaConnectorId | ChapterMangaConnectorId;
|
|
||||||
imageStyle?: CSSProperties;
|
|
||||||
printName?: boolean;
|
|
||||||
}): ReactNode {
|
|
||||||
const mangaConnectorContext = useContext(MangaConnectorContext);
|
|
||||||
const [mangaConnector, setMangaConnector] = useState<
|
|
||||||
MangaConnector | undefined
|
|
||||||
>(
|
|
||||||
mangaConnectorContext?.find(
|
|
||||||
(c) => c.name == MangaConnectorId.mangaConnectorName,
|
|
||||||
),
|
|
||||||
);
|
|
||||||
const imageRef = useRef<HTMLImageElement | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const connector = mangaConnectorContext?.find(
|
|
||||||
(c) => c.name == MangaConnectorId.mangaConnectorName,
|
|
||||||
);
|
|
||||||
setMangaConnector(connector);
|
|
||||||
if (imageRef?.current != null)
|
|
||||||
imageRef.current.setHTMLUnsafe(
|
|
||||||
`<img ref=${imageRef} src=${mangaConnector?.iconUrl} style=${imageStyle}/>`,
|
|
||||||
);
|
|
||||||
}, [MangaConnectorId, imageStyle, mangaConnector, mangaConnectorContext]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<Typography>
|
|
||||||
{MangaConnectorId.mangaConnectorName}:{" "}
|
|
||||||
<Link href={MangaConnectorId.websiteUrl as string}>
|
|
||||||
{MangaConnectorId.websiteUrl}
|
|
||||||
</Link>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<Link href={MangaConnectorId.websiteUrl as string}>
|
|
||||||
<img
|
|
||||||
ref={imageRef}
|
|
||||||
src={mangaConnector?.iconUrl}
|
|
||||||
style={imageStyle}
|
|
||||||
className={"manga-card-badge-icon"}
|
|
||||||
/>
|
|
||||||
{printName ? <Typography>{mangaConnector?.name}</Typography> : null}
|
|
||||||
</Link>
|
|
||||||
</Tooltip>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MangaConnectorLinkFromId({
|
|
||||||
MangaConnectorIdId,
|
|
||||||
imageStyle,
|
|
||||||
printName,
|
|
||||||
}: {
|
|
||||||
MangaConnectorIdId: string;
|
|
||||||
imageStyle?: CSSProperties;
|
|
||||||
printName?: boolean;
|
|
||||||
}): ReactNode {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
|
|
||||||
const [node, setNode] = useState<ReactNode>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Api.queryMangaMangaConnectorIdDetail(MangaConnectorIdId).then(
|
|
||||||
(response) => {
|
|
||||||
if (response.ok)
|
|
||||||
setNode(
|
|
||||||
<MangaConnectorLink
|
|
||||||
key={response.data.key}
|
|
||||||
MangaConnectorId={response.data}
|
|
||||||
imageStyle={{ ...imageStyle, width: "25px" }}
|
|
||||||
printName={printName}
|
|
||||||
/>,
|
|
||||||
);
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}, [Api, MangaConnectorIdId, imageStyle, printName]);
|
|
||||||
|
|
||||||
return node;
|
|
||||||
}
|
|
@@ -1,25 +1,25 @@
|
|||||||
|
.manga-card-badge {
|
||||||
|
margin: 16px 18px 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
.manga-card {
|
.manga-card {
|
||||||
width: 220px;
|
width: 220px;
|
||||||
height: 300px;
|
height: 300px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.manga-cover-blur {
|
.manga-card-cover {
|
||||||
background: linear-gradient(
|
|
||||||
135deg,
|
|
||||||
rgba(245, 169, 184, 0.85) 40%,
|
|
||||||
rgba(91, 206, 250, 0.3)
|
|
||||||
);
|
|
||||||
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
|
||||||
)backdrop-filter: blur(6px);
|
|
||||||
-webkit-backdrop-filter: blur(6px);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.manga-card-badge-icon {
|
.manga-card-cover-blur {
|
||||||
width: 25px;
|
background: linear-gradient(
|
||||||
height: 25px;
|
135deg,
|
||||||
|
rgba(245, 169, 184, 0.85) 40%,
|
||||||
|
rgba(91, 206, 250, 0.3)
|
||||||
|
);
|
||||||
|
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);
|
||||||
|
backdrop-filter: blur(6px);
|
||||||
|
-webkit-backdrop-filter: blur(6px);
|
||||||
}
|
}
|
||||||
|
|
||||||
.manga-modal {
|
.manga-card-content {
|
||||||
width: 90%;
|
|
||||||
margin: auto;
|
|
||||||
}
|
}
|
||||||
|
@@ -1,180 +1,65 @@
|
|||||||
import {
|
import {
|
||||||
Badge,
|
Badge,
|
||||||
Box,
|
Card,
|
||||||
Card,
|
CardContent,
|
||||||
CardContent,
|
CardCover,
|
||||||
CardCover,
|
Skeleton,
|
||||||
Chip,
|
Typography,
|
||||||
Link,
|
} from '@mui/joy'
|
||||||
Modal,
|
import { EventHandler, ReactNode, useContext, useEffect, useState } from 'react'
|
||||||
ModalDialog,
|
import './MangaCard.css'
|
||||||
Stack,
|
import MangaConnectorIcon from './MangaConnectorIcon.tsx'
|
||||||
Tooltip,
|
import { Manga, MinimalManga } from '../../api/data-contracts.ts'
|
||||||
Typography,
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
} from "@mui/joy";
|
|
||||||
import { Manga, MinimalManga } from "../../apiClient/data-contracts.ts";
|
|
||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
ReactNode,
|
|
||||||
SetStateAction,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import "./MangaCard.css";
|
|
||||||
import MangaConnectorBadge from "./MangaConnectorBadge.tsx";
|
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
|
||||||
import MarkdownPreview from "@uiw/react-markdown-preview";
|
|
||||||
import { MangaContext } from "../../App.tsx";
|
|
||||||
import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx";
|
|
||||||
|
|
||||||
export function MangaCard({
|
export default function MangaCard({
|
||||||
manga,
|
mangaDetail,
|
||||||
children,
|
key,
|
||||||
|
onClick,
|
||||||
}: {
|
}: {
|
||||||
manga: MinimalManga | undefined;
|
mangaDetail?: Manga | MinimalManga
|
||||||
children?: ReactNode;
|
key?: string
|
||||||
}) {
|
onClick?: EventHandler<any>
|
||||||
const [open, setOpen] = useState(false);
|
}): ReactNode {
|
||||||
|
const Api = useContext(ApiContext)
|
||||||
|
|
||||||
if (manga === undefined) return PlaceHolderCard();
|
const [manga, setManga] = useState<Manga | MinimalManga | undefined>(
|
||||||
|
mangaDetail
|
||||||
|
)
|
||||||
|
|
||||||
return (
|
useEffect(() => {
|
||||||
<MangaConnectorBadge manga={manga}>
|
if (!key) return
|
||||||
<Card className={"manga-card"} onClick={() => setOpen(true)}>
|
Api.mangaDetail(key).then((data) => {
|
||||||
<CardCover className={"manga-cover"}>
|
if (data.ok) {
|
||||||
<MangaCover mangaId={manga?.key} />
|
setManga(data.data)
|
||||||
</CardCover>
|
}
|
||||||
<CardCover className={"manga-cover-blur"} />
|
})
|
||||||
<CardContent className={"manga-content"}>
|
}, [Api, key])
|
||||||
<Typography level={"title-lg"}>{manga?.name}</Typography>
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
<MangaModal minimalManga={manga} open={open} setOpen={setOpen}>
|
|
||||||
{children}
|
|
||||||
</MangaModal>
|
|
||||||
</MangaConnectorBadge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function MangaModal({
|
return (
|
||||||
minimalManga,
|
<Badge
|
||||||
open,
|
badgeContent={manga?.mangaConnectorIds.map((id) => (
|
||||||
setOpen,
|
<MangaConnectorIcon key={id.mangaConnectorName} />
|
||||||
children,
|
))}
|
||||||
}: {
|
className={'manga-card-badge'}
|
||||||
minimalManga: MinimalManga;
|
|
||||||
open: boolean;
|
|
||||||
setOpen: Dispatch<SetStateAction<boolean>>;
|
|
||||||
children?: ReactNode;
|
|
||||||
}) {
|
|
||||||
const { getManga } = useContext(MangaContext);
|
|
||||||
const [manga, setManga] = useState<Manga>();
|
|
||||||
useEffect(() => {
|
|
||||||
getManga(minimalManga.key).then(setManga);
|
|
||||||
}, [getManga, minimalManga]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}>
|
|
||||||
<ModalDialog style={{ width: "100%" }}>
|
|
||||||
<ModalClose />
|
|
||||||
<Tooltip
|
|
||||||
title={
|
|
||||||
<Stack spacing={1}>
|
|
||||||
{manga?.altTitles?.map((title) => (
|
|
||||||
<Chip>{title.title}</Chip>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
}
|
|
||||||
>
|
>
|
||||||
<Typography level={"h4"} width={"fit-content"}>
|
<Card className={'manga-card'} onClick={onClick}>
|
||||||
{manga?.name ?? minimalManga.name}
|
<CardCover className={'manga-card-cover'}>
|
||||||
</Typography>
|
<img src={'/blahaj.png'} />
|
||||||
</Tooltip>
|
</CardCover>
|
||||||
<Stack direction={"row"} spacing={2}>
|
<CardCover className={'manga-card-cover-blur'} />
|
||||||
<Box key={"Cover"} className={"manga-card"}>
|
<CardContent className={'manga-card-content'}>
|
||||||
<MangaCover mangaId={minimalManga.key} />
|
<Typography level={'h4'}>
|
||||||
</Box>
|
{manga?.name ?? (
|
||||||
<Stack
|
<Skeleton>{stringWithRandomLength()}</Skeleton>
|
||||||
key={"Description"}
|
)}
|
||||||
direction={"column"}
|
</Typography>
|
||||||
sx={{ width: "calc(100% - 230px)" }}
|
</CardContent>
|
||||||
>
|
</Card>
|
||||||
<Stack
|
</Badge>
|
||||||
key={"Tags"}
|
)
|
||||||
direction={"row"}
|
|
||||||
flexWrap={"wrap"}
|
|
||||||
useFlexGap
|
|
||||||
spacing={0.5}
|
|
||||||
>
|
|
||||||
{manga?.mangaConnectorIdsIds?.map((idid) => (
|
|
||||||
<MangaConnectorLinkFromId
|
|
||||||
key={idid}
|
|
||||||
MangaConnectorIdId={idid}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
{manga?.mangaTags?.map((tag) => (
|
|
||||||
<Chip key={tag.tag}>{tag.tag}</Chip>
|
|
||||||
))}
|
|
||||||
{manga?.links?.map((link) => (
|
|
||||||
<Chip key={link.key}>
|
|
||||||
<Link href={link.linkUrl}>{link.linkProvider}</Link>
|
|
||||||
</Chip>
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
<Box sx={{ flexGrow: 1 }}>
|
|
||||||
<MarkdownPreview
|
|
||||||
source={manga?.description ?? "Loading..."}
|
|
||||||
style={{
|
|
||||||
background: "transparent",
|
|
||||||
maxHeight: "50vh",
|
|
||||||
overflowY: "auto",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Stack
|
|
||||||
sx={{ justifySelf: "flex-end", alignSelf: "flex-end" }}
|
|
||||||
spacing={2}
|
|
||||||
direction={"row"}
|
|
||||||
>
|
|
||||||
{manga ? children : null}
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</Stack>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function PlaceHolderCard() {
|
const stringWithRandomLength = (): string => {
|
||||||
return (
|
return 'wow'
|
||||||
<Badge>
|
|
||||||
<Card className={"manga-card"}>
|
|
||||||
<CardCover className={"manga-cover"}>
|
|
||||||
<img src={"/blahaj.png"} />
|
|
||||||
</CardCover>
|
|
||||||
<CardCover className={"manga-cover-blur"} />
|
|
||||||
</Card>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function MangaCover({ mangaId }: { mangaId?: string }) {
|
|
||||||
const api = useContext(ApiContext);
|
|
||||||
const uri = mangaId
|
|
||||||
? `${api.baseUrl}/v2/Manga/${mangaId}/Cover`
|
|
||||||
: "blahaj.png";
|
|
||||||
|
|
||||||
return (
|
|
||||||
<img
|
|
||||||
src={uri}
|
|
||||||
style={{
|
|
||||||
width: "100%",
|
|
||||||
height: "100%",
|
|
||||||
objectFit: "cover",
|
|
||||||
borderRadius: "var(--CardCover-radius)",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,23 +0,0 @@
|
|||||||
import { Badge } from "@mui/joy";
|
|
||||||
import { MinimalManga } from "../../apiClient/data-contracts.ts";
|
|
||||||
import { ReactNode } from "react";
|
|
||||||
import "./MangaCard.css";
|
|
||||||
import MangaConnectorLink from "../MangaConnectorLink.tsx";
|
|
||||||
|
|
||||||
export default function MangaConnectorBadge({
|
|
||||||
manga,
|
|
||||||
children,
|
|
||||||
}: {
|
|
||||||
manga: MinimalManga;
|
|
||||||
children?: ReactNode;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Badge
|
|
||||||
badgeContent={manga.mangaConnectorIds?.map((id) => (
|
|
||||||
<MangaConnectorLink key={id.key} MangaConnectorId={id} />
|
|
||||||
))}
|
|
||||||
>
|
|
||||||
{children}
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
41
tranga-website/src/Components/Mangas/MangaConnectorIcon.tsx
Normal file
41
tranga-website/src/Components/Mangas/MangaConnectorIcon.tsx
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
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,
|
||||||
|
key,
|
||||||
|
}: {
|
||||||
|
mangaConnector?: MangaConnector
|
||||||
|
key?: string
|
||||||
|
}): ReactNode {
|
||||||
|
const Api = useContext(ApiContext)
|
||||||
|
|
||||||
|
const [connector, setConnector] = useState<MangaConnector | undefined>(
|
||||||
|
mangaConnector
|
||||||
|
)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (mangaConnector) {
|
||||||
|
setConnector(mangaConnector)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (!key) return
|
||||||
|
Api.mangaConnectorDetail(key).then((result) => {
|
||||||
|
if (result.ok) {
|
||||||
|
setConnector(result.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [Api, key, mangaConnector])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip title={connector?.name ?? 'loading'}>
|
||||||
|
<img
|
||||||
|
src={connector?.iconUrl ?? '/blahaj.png'}
|
||||||
|
width={'25px'}
|
||||||
|
height={'25px'}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
)
|
||||||
|
}
|
@@ -1,148 +0,0 @@
|
|||||||
import { Manga } from "../../apiClient/data-contracts.ts";
|
|
||||||
import {
|
|
||||||
ChangeEvent,
|
|
||||||
Dispatch,
|
|
||||||
ReactNode,
|
|
||||||
useContext,
|
|
||||||
useEffect,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import { Button, Checkbox, Option, Select, Stack, Typography } from "@mui/joy";
|
|
||||||
import Drawer from "@mui/joy/Drawer";
|
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
|
||||||
import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx";
|
|
||||||
import Sheet from "@mui/joy/Sheet";
|
|
||||||
import { FileLibraryContext, MangaContext } from "../../App.tsx";
|
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
|
||||||
import { LoadingState, StateIndicator } from "../Loading.tsx";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export default function ({ mangaId }: { mangaId: string }): ReactNode {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const { getManga } = useContext(MangaContext);
|
|
||||||
const [manga, setManga] = useState<Manga>();
|
|
||||||
useEffect(() => {
|
|
||||||
getManga(mangaId).then(setManga);
|
|
||||||
}, [getManga, mangaId]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={() => setOpen(true)}>Download</Button>
|
|
||||||
<DownloadDrawer manga={manga} open={open} setOpen={setOpen} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DownloadDrawer({
|
|
||||||
manga,
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: {
|
|
||||||
manga: Manga | undefined;
|
|
||||||
open: boolean;
|
|
||||||
setOpen: Dispatch<boolean>;
|
|
||||||
}): ReactNode {
|
|
||||||
const fileLibraries = useContext(FileLibraryContext);
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
|
|
||||||
const onLibraryChange = (
|
|
||||||
_: React.MouseEvent | React.KeyboardEvent | React.FocusEvent | null,
|
|
||||||
value: string | null,
|
|
||||||
) => {
|
|
||||||
if (!value) return;
|
|
||||||
if (!manga) return;
|
|
||||||
Api.mangaChangeLibraryCreate(manga.key, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Drawer open={open} onClose={() => setOpen(false)}>
|
|
||||||
<ModalClose />
|
|
||||||
<Sheet sx={{ width: "calc(95% - 60px)", margin: "30px" }}>
|
|
||||||
<Typography>Download to Library:</Typography>
|
|
||||||
<Select
|
|
||||||
placeholder={"Library"}
|
|
||||||
onChange={onLibraryChange}
|
|
||||||
value={manga?.libraryId}
|
|
||||||
>
|
|
||||||
{fileLibraries?.map((library) => (
|
|
||||||
<Option value={library.key} key={library.key}>
|
|
||||||
<Typography>{library.libraryName}</Typography>{" "}
|
|
||||||
<Typography>({library.basePath})</Typography>
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
<Typography>Download from:</Typography>
|
|
||||||
<Stack>
|
|
||||||
{manga?.mangaConnectorIdsIds?.map((id) => (
|
|
||||||
<DownloadCheckBox key={id} mangaConnectorIdId={id} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
</Sheet>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function DownloadCheckBox({
|
|
||||||
mangaConnectorIdId,
|
|
||||||
}: {
|
|
||||||
mangaConnectorIdId: string;
|
|
||||||
}): ReactNode {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
const [useForDownloading, setUseForDownloading] = useState<boolean>(false);
|
|
||||||
const [loading, setLoading] = useState<LoadingState>(LoadingState.none);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setLoading(LoadingState.loading);
|
|
||||||
Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
setUseForDownloading(response.data.useForDownload as boolean);
|
|
||||||
setLoading(LoadingState.none);
|
|
||||||
} else setLoading(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch((_) => setLoading(LoadingState.failure));
|
|
||||||
}, [Api, mangaConnectorIdId]);
|
|
||||||
|
|
||||||
const onSelected = (event: ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setLoading(LoadingState.loading);
|
|
||||||
const val = event.currentTarget.checked;
|
|
||||||
Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId)
|
|
||||||
.then((response) => {
|
|
||||||
if (!response.ok) {
|
|
||||||
setLoading(LoadingState.failure);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Api.mangaSetAsDownloadFromCreate(
|
|
||||||
response.data.objId,
|
|
||||||
response.data.mangaConnectorName,
|
|
||||||
val,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
setUseForDownloading(val);
|
|
||||||
setLoading(LoadingState.success);
|
|
||||||
} else setLoading(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch((_) => setLoading(LoadingState.failure));
|
|
||||||
})
|
|
||||||
.catch((_) => setLoading(LoadingState.failure));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Checkbox
|
|
||||||
indeterminateIcon={StateIndicator(LoadingState.loading)}
|
|
||||||
indeterminate={loading === LoadingState.loading}
|
|
||||||
disabled={loading === LoadingState.loading}
|
|
||||||
checked={useForDownloading}
|
|
||||||
onChange={onSelected}
|
|
||||||
label={
|
|
||||||
<Typography>
|
|
||||||
<MangaConnectorLinkFromId
|
|
||||||
MangaConnectorIdId={mangaConnectorIdId}
|
|
||||||
printName={true}
|
|
||||||
/>
|
|
||||||
</Typography>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,3 +0,0 @@
|
|||||||
.manga-list {
|
|
||||||
padding-top: 12px;
|
|
||||||
}
|
|
||||||
|
@@ -1,38 +1,72 @@
|
|||||||
import { ReactNode } from "react";
|
import { Stack } from '@mui/joy'
|
||||||
import { MangaCard } from "./MangaCard.tsx";
|
import './MangaList.css'
|
||||||
import { Stack } from "@mui/joy";
|
import { ReactNode, useContext, useEffect, useState } from 'react'
|
||||||
import "./MangaList.css";
|
import {
|
||||||
import { MinimalManga } from "../../apiClient/data-contracts.ts";
|
Manga,
|
||||||
import MangaDownloadDialog from "./MangaDownloadDialog.tsx";
|
MangaReleaseStatus,
|
||||||
import MangaMerge from "./MangaMerge.tsx";
|
MinimalManga,
|
||||||
|
} from '../../api/data-contracts.ts'
|
||||||
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
|
import MangaCard from './MangaCard.tsx'
|
||||||
|
|
||||||
export default function MangaList({
|
export default function MangaList({
|
||||||
manga,
|
openSearch,
|
||||||
children,
|
|
||||||
}: {
|
}: {
|
||||||
manga: MinimalManga[];
|
openSearch: () => void
|
||||||
children?: ReactNode;
|
}): ReactNode {
|
||||||
}) {
|
const Api = useContext(ApiContext)
|
||||||
return (
|
const [downloadingManga, setDownloadingManga] = useState<MinimalManga[]>([])
|
||||||
<Stack
|
|
||||||
className={"manga-list"}
|
useEffect(() => {
|
||||||
direction={"row"}
|
Api.mangaDownloadingList().then((data) => {
|
||||||
useFlexGap={true}
|
if (data.ok) {
|
||||||
spacing={2}
|
setDownloadingManga(data.data)
|
||||||
flexWrap={"wrap"}
|
}
|
||||||
sx={{
|
})
|
||||||
mx: "calc(-1 * var(--ModalDialog-padding))",
|
}, [Api])
|
||||||
px: "var(--ModalDialog-padding)",
|
|
||||||
overflowY: "scroll",
|
return (
|
||||||
}}
|
<MangaCardList manga={downloadingManga}>
|
||||||
>
|
<MangaCard
|
||||||
{children}
|
onClick={openSearch}
|
||||||
{manga?.map((minimalManga) => (
|
mangaDetail={{
|
||||||
<MangaCard key={minimalManga.key} manga={minimalManga}>
|
name: 'Search',
|
||||||
<MangaDownloadDialog mangaId={minimalManga.key} />
|
description: 'Search for a new Manga',
|
||||||
<MangaMerge manga={minimalManga} />
|
releaseStatus: MangaReleaseStatus.Continuing,
|
||||||
</MangaCard>
|
mangaConnectorIds: [],
|
||||||
))}
|
key: 'Search',
|
||||||
</Stack>
|
}}
|
||||||
);
|
/>
|
||||||
|
</MangaCardList>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
export function MangaCardList({
|
||||||
|
manga,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
manga: (Manga | MinimalManga)[]
|
||||||
|
children?: ReactNode
|
||||||
|
}): ReactNode {
|
||||||
|
return (
|
||||||
|
<Stack
|
||||||
|
className={'manga-list'}
|
||||||
|
direction={'row'}
|
||||||
|
flexWrap={'wrap'}
|
||||||
|
sx={{
|
||||||
|
mx: 'calc(-1 * var(--ModalDialog-padding))',
|
||||||
|
px: 'var(--ModalDialog-padding)',
|
||||||
|
overflowY: 'scroll',
|
||||||
|
justifyItems: 'space-between',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
{manga.map((m) => (
|
||||||
|
<>
|
||||||
|
<MangaCard mangaDetail={m} />
|
||||||
|
<span style={{ flexGrow: 1 }} />
|
||||||
|
</>
|
||||||
|
))}
|
||||||
|
</Stack>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,143 +0,0 @@
|
|||||||
import { ReactNode, useContext, useEffect, useState } from "react";
|
|
||||||
import { Manga, MinimalManga } from "../../apiClient/data-contracts.ts";
|
|
||||||
import Drawer from "@mui/joy/Drawer";
|
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
|
||||||
import { MangaCard } from "./MangaCard.tsx";
|
|
||||||
import {
|
|
||||||
Alert,
|
|
||||||
Button,
|
|
||||||
Modal,
|
|
||||||
ModalDialog,
|
|
||||||
Stack,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import { KeyboardDoubleArrowRight, Warning } from "@mui/icons-material";
|
|
||||||
import { LoadingState, StateIndicator } from "../Loading.tsx";
|
|
||||||
|
|
||||||
export default function ({
|
|
||||||
manga,
|
|
||||||
}: {
|
|
||||||
manga: MinimalManga | undefined;
|
|
||||||
}): ReactNode {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
|
|
||||||
const [similar, setSimilar] = useState<Manga[]>();
|
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (manga === undefined || !open) return;
|
|
||||||
Api.queryMangaSimilarNameList(manga.key as string).then((response) => {
|
|
||||||
if (response.ok)
|
|
||||||
Api.mangaWithIDsCreate(response.data).then((response) => {
|
|
||||||
if (response.ok) setSimilar(response.data);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}, [Api, manga, open]);
|
|
||||||
|
|
||||||
const exit = (manga: Manga) => {
|
|
||||||
setOpen(false);
|
|
||||||
setSimilar(similar?.filter((m) => m.key != manga.key));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={(_) => setOpen(true)}>Merge</Button>
|
|
||||||
<Drawer
|
|
||||||
size={"lg"}
|
|
||||||
open={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
anchor={"bottom"}
|
|
||||||
>
|
|
||||||
<ModalClose />
|
|
||||||
<Typography>Merge targets: {similar?.length ?? 0}</Typography>
|
|
||||||
<Stack direction={"row"} spacing={2} flexWrap={"wrap"} useFlexGap>
|
|
||||||
{similar
|
|
||||||
? similar?.map((similarManga) => (
|
|
||||||
<MangaCard manga={similarManga}>
|
|
||||||
<ConfirmationModal
|
|
||||||
manga={manga as Manga}
|
|
||||||
similarManga={similarManga}
|
|
||||||
exit={() => exit(similarManga)}
|
|
||||||
/>
|
|
||||||
</MangaCard>
|
|
||||||
))
|
|
||||||
: "Loading..."}
|
|
||||||
</Stack>
|
|
||||||
</Drawer>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function ConfirmationModal({
|
|
||||||
manga,
|
|
||||||
similarManga,
|
|
||||||
exit,
|
|
||||||
}: {
|
|
||||||
manga: Manga;
|
|
||||||
similarManga: Manga;
|
|
||||||
exit: () => void;
|
|
||||||
}): ReactNode {
|
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
|
|
||||||
const [loadingState, setLoadingState] = useState<LoadingState>(
|
|
||||||
LoadingState.none,
|
|
||||||
);
|
|
||||||
|
|
||||||
const merge = () => {
|
|
||||||
setLoadingState(LoadingState.loading);
|
|
||||||
Api.mangaMergeIntoPartialUpdate(
|
|
||||||
manga.key as string,
|
|
||||||
similarManga.key as string,
|
|
||||||
)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
setLoadingState(LoadingState.success);
|
|
||||||
setOpen(false);
|
|
||||||
exit();
|
|
||||||
} else setLoadingState(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch((_) => setLoadingState(LoadingState.failure));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{loadingState == LoadingState.success ? (
|
|
||||||
<Alert
|
|
||||||
color="success"
|
|
||||||
startDecorator={StateIndicator(LoadingState.success)}
|
|
||||||
>
|
|
||||||
Merged Successfully!
|
|
||||||
</Alert>
|
|
||||||
) : (
|
|
||||||
<Button onClick={(_) => setOpen(true)}>Merge into</Button>
|
|
||||||
)}
|
|
||||||
<Modal open={open} onClose={(_) => setOpen(false)}>
|
|
||||||
<ModalDialog>
|
|
||||||
<ModalClose />
|
|
||||||
<Typography level={"h2"}>Confirm Merge</Typography>
|
|
||||||
<Stack direction={"row"} spacing={3} alignItems={"center"}>
|
|
||||||
<MangaCard manga={manga} />
|
|
||||||
<Typography color={"danger"} level={"h1"}>
|
|
||||||
<KeyboardDoubleArrowRight />
|
|
||||||
</Typography>
|
|
||||||
<MangaCard manga={similarManga} />
|
|
||||||
</Stack>
|
|
||||||
<Tooltip title={"THIS CAN NOT BE UNDONE!"}>
|
|
||||||
<Button
|
|
||||||
onClick={merge}
|
|
||||||
disabled={loadingState === LoadingState.loading}
|
|
||||||
endDecorator={StateIndicator(loadingState)}
|
|
||||||
color={"danger"}
|
|
||||||
startDecorator={<Warning />}
|
|
||||||
>
|
|
||||||
Merge
|
|
||||||
</Button>
|
|
||||||
</Tooltip>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,175 +0,0 @@
|
|||||||
import {
|
|
||||||
Dispatch,
|
|
||||||
KeyboardEventHandler,
|
|
||||||
ReactNode,
|
|
||||||
useContext,
|
|
||||||
useState,
|
|
||||||
} from "react";
|
|
||||||
import {
|
|
||||||
Badge,
|
|
||||||
Button,
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardCover,
|
|
||||||
Chip,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalDialog,
|
|
||||||
Option,
|
|
||||||
Select,
|
|
||||||
Step,
|
|
||||||
StepIndicator,
|
|
||||||
Stepper,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
|
||||||
import { MangaConnectorContext } from "../App.tsx";
|
|
||||||
import { MangaConnector, MinimalManga } from "../apiClient/data-contracts.ts";
|
|
||||||
import MangaList from "./Mangas/MangaList.tsx";
|
|
||||||
import { ApiContext } from "../apiClient/ApiContext.tsx";
|
|
||||||
import { LoadingState, StateColor, StateIndicator } from "./Loading.tsx";
|
|
||||||
|
|
||||||
export default function (): ReactNode {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Badge badgeContent={"+"}>
|
|
||||||
<Card
|
|
||||||
onClick={() => {
|
|
||||||
if (!open) setOpen(true);
|
|
||||||
}}
|
|
||||||
className={"manga-card"}
|
|
||||||
>
|
|
||||||
<CardCover className={"manga-cover"}>
|
|
||||||
<img src={"/blahaj.png"} />
|
|
||||||
</CardCover>
|
|
||||||
<CardCover className={"manga-cover-blur"} />
|
|
||||||
<CardContent>Add</CardContent>
|
|
||||||
<CardContent>
|
|
||||||
<SearchDialog open={open} setOpen={setOpen} />
|
|
||||||
</CardContent>
|
|
||||||
</Card>
|
|
||||||
</Badge>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function SearchDialog({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: Dispatch<boolean>;
|
|
||||||
}): ReactNode {
|
|
||||||
const mangaConnectors = useContext(MangaConnectorContext);
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
|
|
||||||
const [selectedMangaConnector, setSelectedMangaConnector] = useState<
|
|
||||||
MangaConnector | undefined
|
|
||||||
>(undefined);
|
|
||||||
const [searchTerm, setSearchTerm] = useState<string>();
|
|
||||||
const [searchResults, setSearchResults] = useState<MinimalManga[]>([]);
|
|
||||||
|
|
||||||
const [loadingState, setLoadingState] = useState<LoadingState>(
|
|
||||||
LoadingState.none,
|
|
||||||
);
|
|
||||||
|
|
||||||
const doTheSearch = () => {
|
|
||||||
if (searchTerm === undefined || searchTerm.length < 1) return;
|
|
||||||
if (!isUrl(searchTerm) && selectedMangaConnector === undefined) return;
|
|
||||||
|
|
||||||
setLoadingState(LoadingState.loading);
|
|
||||||
|
|
||||||
if (isUrl(searchTerm))
|
|
||||||
Api.searchUrlCreate(searchTerm)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
setSearchResults([response.data]);
|
|
||||||
setLoadingState(LoadingState.success);
|
|
||||||
} else setLoadingState(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch(() => setLoadingState(LoadingState.failure));
|
|
||||||
else
|
|
||||||
Api.searchDetail(selectedMangaConnector!.name, searchTerm)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
setSearchResults(response.data);
|
|
||||||
setLoadingState(LoadingState.success);
|
|
||||||
} else setLoadingState(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch(() => setLoadingState(LoadingState.failure));
|
|
||||||
};
|
|
||||||
|
|
||||||
const isUrl = (url: string) => {
|
|
||||||
try {
|
|
||||||
new URL(url);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const keyDownCheck: KeyboardEventHandler<HTMLInputElement> = (e) => {
|
|
||||||
if (e.key === "Enter") {
|
|
||||||
doTheSearch();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Modal
|
|
||||||
sx={{ width: "100%", height: "100%" }}
|
|
||||||
open={open}
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<ModalDialog sx={{ width: "80%" }}>
|
|
||||||
<ModalClose />
|
|
||||||
<Stepper orientation={"vertical"}>
|
|
||||||
<Step indicator={<StepIndicator>1</StepIndicator>}>
|
|
||||||
<Typography>Connector</Typography>
|
|
||||||
<Select
|
|
||||||
disabled={loadingState == LoadingState.loading}
|
|
||||||
onChange={(_, v) =>
|
|
||||||
setSelectedMangaConnector(v as MangaConnector)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{mangaConnectors?.map((con) => (
|
|
||||||
<Option value={con}>
|
|
||||||
<Typography>
|
|
||||||
<img
|
|
||||||
src={con.iconUrl}
|
|
||||||
style={{ maxHeight: "var(--Icon-fontSize)" }}
|
|
||||||
/>
|
|
||||||
{con.name}
|
|
||||||
</Typography>
|
|
||||||
</Option>
|
|
||||||
))}
|
|
||||||
</Select>
|
|
||||||
</Step>
|
|
||||||
<Step indicator={<StepIndicator>2</StepIndicator>}>
|
|
||||||
<Typography>Search</Typography>
|
|
||||||
<Input
|
|
||||||
disabled={loadingState == LoadingState.loading}
|
|
||||||
onKeyDown={keyDownCheck}
|
|
||||||
onChange={(e) => setSearchTerm(e.currentTarget.value)}
|
|
||||||
endDecorator={
|
|
||||||
<Button
|
|
||||||
disabled={loadingState == LoadingState.loading}
|
|
||||||
onClick={doTheSearch}
|
|
||||||
endDecorator={StateIndicator(loadingState)}
|
|
||||||
color={StateColor(loadingState)}
|
|
||||||
>
|
|
||||||
Search
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Step>
|
|
||||||
<Step indicator={<StepIndicator>3</StepIndicator>}>
|
|
||||||
<Typography>
|
|
||||||
Result <Chip>{searchResults.length}</Chip>
|
|
||||||
</Typography>
|
|
||||||
<MangaList manga={searchResults} />
|
|
||||||
</Step>
|
|
||||||
</Stepper>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,43 +1,41 @@
|
|||||||
import { ReactNode, useContext, useState } from "react";
|
import { ReactNode, useContext } from 'react'
|
||||||
import { SettingsContext, SettingsItem } from "./Settings.tsx";
|
import { SettingsContext, SettingsItem } from './Settings.tsx'
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
import { ColorPaletteProp, Input } from "@mui/joy";
|
import MarkdownPreview from '@uiw/react-markdown-preview'
|
||||||
import * as React from "react";
|
import TInput from '../Inputs/TInput.tsx'
|
||||||
import MarkdownPreview from "@uiw/react-markdown-preview";
|
|
||||||
|
|
||||||
export default function (): ReactNode {
|
export default function ChapterNamingScheme(): ReactNode {
|
||||||
const settings = useContext(SettingsContext);
|
const settings = useContext(SettingsContext)
|
||||||
const Api = useContext(ApiContext);
|
const Api = useContext(ApiContext)
|
||||||
|
|
||||||
const [scheme, setScheme] = useState<ColorPaletteProp>("neutral");
|
const schemeChanged = async (
|
||||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
value: string | number | readonly string[] | undefined
|
||||||
const schemeChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
) => {
|
||||||
clearTimeout(timerRef.current);
|
if (typeof value != 'string') return Promise.reject()
|
||||||
setScheme("warning");
|
try {
|
||||||
timerRef.current = setTimeout(() => {
|
const response =
|
||||||
Api.settingsChapterNamingSchemePartialUpdate(e.target.value)
|
await Api.settingsChapterNamingSchemePartialUpdate(value)
|
||||||
.then((response) => {
|
if (response.ok) return Promise.resolve()
|
||||||
if (response.ok) setScheme("success");
|
else return Promise.reject()
|
||||||
else setScheme("danger");
|
} catch {
|
||||||
})
|
return await Promise.reject()
|
||||||
.catch(() => setScheme("danger"));
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<SettingsItem title={"Chapter Naming Scheme"}>
|
|
||||||
<MarkdownPreview
|
|
||||||
style={{ backgroundColor: "transparent" }}
|
|
||||||
source={
|
|
||||||
"Placeholders:\n * %M Obj Name\n * %V Volume\n * %C Chapter\n * %T Title\n * %A Author (first in list)\n * %I Chapter Internal ID\n * %i Obj Internal ID\n * %Y Year (Obj)\n *\n * ?_(...) replace _ with a value from above:\n * Everything inside the braces will only be added if the value of %_ is not null"
|
|
||||||
}
|
}
|
||||||
/>
|
}
|
||||||
<Input
|
|
||||||
color={scheme}
|
return (
|
||||||
defaultValue={settings?.chapterNamingScheme as string}
|
<SettingsItem title={'Chapter Naming Scheme'}>
|
||||||
placeholder={"Scheme"}
|
<MarkdownPreview
|
||||||
onChange={schemeChanged}
|
style={{ backgroundColor: 'transparent' }}
|
||||||
/>
|
source={
|
||||||
</SettingsItem>
|
'Placeholders:\n * %M Obj Name\n * %V Volume\n * %C Chapter\n * %T Title\n * %A Author (first in list)\n * %I Chapter Internal ID\n * %i Obj Internal ID\n * %Y Year (Obj)\n *\n * ?_(...) replace _ with a value from above:\n * Everything inside the braces will only be added if the value of %_ is not null'
|
||||||
);
|
}
|
||||||
|
/>
|
||||||
|
<TInput
|
||||||
|
defaultValue={settings?.chapterNamingScheme as string}
|
||||||
|
placeholder={'Scheme'}
|
||||||
|
completionAction={schemeChanged}
|
||||||
|
actionDelay={5000}
|
||||||
|
/>
|
||||||
|
</SettingsItem>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,14 +1,14 @@
|
|||||||
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 () {
|
export default function Download() {
|
||||||
return (
|
return (
|
||||||
<SettingsItem title={"Download"}>
|
<SettingsItem title={'Download'}>
|
||||||
<ImageCompression />
|
<ImageCompression />
|
||||||
<DownloadLanguage />
|
<DownloadLanguage />
|
||||||
<ChapterNamingScheme />
|
<ChapterNamingScheme />
|
||||||
</SettingsItem>
|
</SettingsItem>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,36 +1,33 @@
|
|||||||
import { ReactNode, useContext, useState } from "react";
|
import { ReactNode, useContext } from 'react'
|
||||||
import { SettingsContext, SettingsItem } from "./Settings.tsx";
|
import { SettingsContext, SettingsItem } from './Settings.tsx'
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
import { ColorPaletteProp, Input } from "@mui/joy";
|
import TInput from '../Inputs/TInput.tsx'
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export default function (): ReactNode {
|
export default function DownloadLanguage(): ReactNode {
|
||||||
const settings = useContext(SettingsContext);
|
const settings = useContext(SettingsContext)
|
||||||
const Api = useContext(ApiContext);
|
const Api = useContext(ApiContext)
|
||||||
|
|
||||||
const [color, setColor] = useState<ColorPaletteProp>("neutral");
|
const languageChanged = async (
|
||||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
value: string | number | readonly string[] | undefined
|
||||||
const languageChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
) => {
|
||||||
clearTimeout(timerRef.current);
|
if (typeof value != 'string') return Promise.reject()
|
||||||
setColor("warning");
|
try {
|
||||||
timerRef.current = setTimeout(() => {
|
const response =
|
||||||
Api.settingsDownloadLanguagePartialUpdate(e.target.value)
|
await Api.settingsDownloadLanguagePartialUpdate(value)
|
||||||
.then((response) => {
|
if (response.ok) return Promise.resolve()
|
||||||
if (response.ok) setColor("success");
|
else return Promise.reject()
|
||||||
else setColor("danger");
|
} catch {
|
||||||
})
|
return await Promise.reject()
|
||||||
.catch(() => setColor("danger"));
|
}
|
||||||
}, 1000);
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsItem title={"Download Language"}>
|
<SettingsItem title={'Download Language'}>
|
||||||
<Input
|
<TInput
|
||||||
color={color}
|
defaultValue={settings?.downloadLanguage as string}
|
||||||
defaultValue={settings?.downloadLanguage as string}
|
placeholder={"Language code (f.e. 'en')"}
|
||||||
placeholder={"Language code (f.e. 'en')"}
|
completionAction={languageChanged}
|
||||||
onChange={languageChanged}
|
/>
|
||||||
/>
|
</SettingsItem>
|
||||||
</SettingsItem>
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,37 +1,32 @@
|
|||||||
import { ReactNode, useContext, useState } from "react";
|
import { ReactNode, useContext } from 'react'
|
||||||
import { SettingsContext, SettingsItem } from "./Settings.tsx";
|
import { SettingsContext, SettingsItem } from './Settings.tsx'
|
||||||
import { ColorPaletteProp, Input } from "@mui/joy";
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
import * as React from "react";
|
import TInput from '../Inputs/TInput.tsx'
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
|
||||||
|
|
||||||
export default function (): ReactNode {
|
export default function FlareSolverr(): ReactNode {
|
||||||
const settings = useContext(SettingsContext);
|
const settings = useContext(SettingsContext)
|
||||||
const Api = useContext(ApiContext);
|
const Api = useContext(ApiContext)
|
||||||
|
|
||||||
const [uriColor, setUriColor] = useState<ColorPaletteProp>("neutral");
|
const uriChanged = async (
|
||||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
value: string | number | readonly string[] | undefined
|
||||||
const uriChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
) => {
|
||||||
clearTimeout(timerRef.current);
|
if (typeof value != 'string') return Promise.reject()
|
||||||
setUriColor("warning");
|
try {
|
||||||
timerRef.current = setTimeout(() => {
|
const response = await Api.settingsFlareSolverrUrlCreate(value)
|
||||||
Api.settingsFlareSolverrUrlCreate(e.target.value)
|
if (response.ok) return Promise.resolve()
|
||||||
.then((response) => {
|
else return Promise.reject()
|
||||||
if (response.ok) setUriColor("success");
|
} catch (reason) {
|
||||||
else setUriColor("danger");
|
return await Promise.reject(reason)
|
||||||
})
|
}
|
||||||
.catch(() => setUriColor("danger"));
|
}
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsItem title={"FlareSolverr"}>
|
<SettingsItem title={'FlareSolverr'}>
|
||||||
<Input
|
<TInput
|
||||||
color={uriColor}
|
placeholder={'FlareSolverr URL'}
|
||||||
defaultValue={settings?.flareSolverrUrl as string}
|
defaultValue={settings?.flareSolverrUrl as string}
|
||||||
type={"url"}
|
completionAction={uriChanged}
|
||||||
placeholder={"URL"}
|
/>
|
||||||
onChange={uriChanged}
|
</SettingsItem>
|
||||||
/>
|
)
|
||||||
</SettingsItem>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,17 +1,17 @@
|
|||||||
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 (): 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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,25 +0,0 @@
|
|||||||
import { Modal, ModalDialog, Tab, TabList, Tabs } from "@mui/joy";
|
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
|
||||||
import { Dispatch } from "react";
|
|
||||||
|
|
||||||
export default function ({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: Dispatch<boolean>;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Modal open={open} onClose={() => setOpen(false)}>
|
|
||||||
<ModalDialog>
|
|
||||||
<ModalClose />
|
|
||||||
<Tabs sx={{ width: "95%" }} defaultValue={"komga"}>
|
|
||||||
<TabList>
|
|
||||||
<Tab value={"komga"}>Komga</Tab>
|
|
||||||
<Tab value={"kavita"}>Kavita</Tab>
|
|
||||||
</TabList>
|
|
||||||
</Tabs>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,20 +0,0 @@
|
|||||||
import { Button, Card, Typography } from "@mui/joy";
|
|
||||||
import { useState } from "react";
|
|
||||||
import ListLibraryConnectors from "./ListLibraryConnectors.tsx";
|
|
||||||
import AddLibraryConnector from "./AddLibraryConnector.tsx";
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const [addDialogOpen, setAddDialogOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Typography>Library Connectors</Typography>
|
|
||||||
<Button onClick={() => setAddDialogOpen(true)}>Add</Button>
|
|
||||||
<ListLibraryConnectors />
|
|
||||||
<AddLibraryConnector
|
|
||||||
open={addDialogOpen}
|
|
||||||
setOpen={() => setAddDialogOpen(false)}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,34 +0,0 @@
|
|||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import { ApiContext } from "../../../apiClient/ApiContext.tsx";
|
|
||||||
import { LibraryConnector } from "../../../apiClient/data-contracts.ts";
|
|
||||||
import { Card, Chip, Input, Stack } from "@mui/joy";
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
const [libraryConnectors, setLibraryConnectors] = useState<
|
|
||||||
LibraryConnector[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Api.libraryConnectorList().then((r) => {
|
|
||||||
if (r.ok) setLibraryConnectors(r.data);
|
|
||||||
});
|
|
||||||
}, [Api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack direction={"column"} spacing={1}>
|
|
||||||
{libraryConnectors.map((c) => (
|
|
||||||
<LibraryConnectorItem key={c.key} connector={c} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function LibraryConnectorItem({ connector }: { connector: LibraryConnector }) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Chip>{connector.libraryType}</Chip>
|
|
||||||
<Input disabled value={connector.baseUrl} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,33 +1,26 @@
|
|||||||
import { Button } from "@mui/joy";
|
import { SettingsItem } from './Settings.tsx'
|
||||||
import { SettingsItem } from "./Settings.tsx";
|
import { useContext } from 'react'
|
||||||
import { useContext, useState } from "react";
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
import TButton from '../Inputs/TButton.tsx'
|
||||||
import { LoadingState, StateColor, StateIndicator } from "../Loading.tsx";
|
|
||||||
|
|
||||||
export default function () {
|
export default function Maintenance() {
|
||||||
const Api = useContext(ApiContext);
|
const Api = useContext(ApiContext)
|
||||||
|
|
||||||
const [unusedMangaState, setUnusedMangaState] = useState(LoadingState.none);
|
const cleanUnusedManga = async (): Promise<void> => {
|
||||||
const cleanUnusedManga = () => {
|
try {
|
||||||
setUnusedMangaState(LoadingState.loading);
|
const result = await Api.maintenanceCleanupNoDownloadMangaCreate()
|
||||||
Api.maintenanceCleanupNoDownloadMangaCreate()
|
if (result.ok) return Promise.resolve()
|
||||||
.then((r) => {
|
else return Promise.reject()
|
||||||
if (r.ok) setUnusedMangaState(LoadingState.success);
|
} catch (reason) {
|
||||||
else setUnusedMangaState(LoadingState.failure);
|
return await Promise.reject(reason)
|
||||||
})
|
}
|
||||||
.catch((_) => setUnusedMangaState(LoadingState.failure));
|
}
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<SettingsItem title={"Maintenance"}>
|
<SettingsItem title={'Maintenance'}>
|
||||||
<Button
|
<TButton completionAction={cleanUnusedManga}>
|
||||||
disabled={unusedMangaState == LoadingState.loading}
|
Cleanup unused Manga
|
||||||
color={StateColor(unusedMangaState)}
|
</TButton>
|
||||||
endDecorator={StateIndicator(unusedMangaState)}
|
</SettingsItem>
|
||||||
onClick={cleanUnusedManga}
|
)
|
||||||
>
|
|
||||||
Cleanup unused Manga
|
|
||||||
</Button>
|
|
||||||
</SettingsItem>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,232 +0,0 @@
|
|||||||
import { ReactNode, useContext, useState } from "react";
|
|
||||||
import { ApiContext } from "../../../apiClient/ApiContext";
|
|
||||||
import {
|
|
||||||
Button,
|
|
||||||
Input,
|
|
||||||
Modal,
|
|
||||||
ModalDialog,
|
|
||||||
Stack,
|
|
||||||
Tab,
|
|
||||||
TabList,
|
|
||||||
TabPanel,
|
|
||||||
Tabs,
|
|
||||||
} from "@mui/joy";
|
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
|
||||||
import {
|
|
||||||
GotifyRecord,
|
|
||||||
NtfyRecord,
|
|
||||||
PushoverRecord,
|
|
||||||
} from "../../../apiClient/data-contracts.ts";
|
|
||||||
import { LoadingState, StateColor, StateIndicator } from "../../Loading.tsx";
|
|
||||||
import * as React from "react";
|
|
||||||
|
|
||||||
export default function ({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: (open: boolean) => void;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Modal open={open} onClose={() => setOpen(false)}>
|
|
||||||
<ModalDialog>
|
|
||||||
<ModalClose />
|
|
||||||
<Tabs sx={{ width: "95%" }} defaultValue={"gotify"}>
|
|
||||||
<TabList>
|
|
||||||
<Tab value={"gotify"}>Gotify</Tab>
|
|
||||||
<Tab value={"ntfy"}>Ntfy</Tab>
|
|
||||||
<Tab value={"pushover"}>Pushover</Tab>
|
|
||||||
</TabList>
|
|
||||||
<Gotify />
|
|
||||||
<Ntfy />
|
|
||||||
<Pushover />
|
|
||||||
</Tabs>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationConnectorTab({
|
|
||||||
value,
|
|
||||||
children,
|
|
||||||
add,
|
|
||||||
state,
|
|
||||||
}: {
|
|
||||||
value: string;
|
|
||||||
children: ReactNode;
|
|
||||||
add: React.MouseEventHandler<HTMLAnchorElement> | undefined;
|
|
||||||
state: LoadingState;
|
|
||||||
}) {
|
|
||||||
const IsLoading = (state: LoadingState): boolean =>
|
|
||||||
state === LoadingState.loading;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<TabPanel value={value}>
|
|
||||||
<Stack spacing={1}>
|
|
||||||
{children}
|
|
||||||
<Button
|
|
||||||
onClick={add}
|
|
||||||
endDecorator={StateIndicator(state)}
|
|
||||||
loading={IsLoading(state)}
|
|
||||||
disabled={IsLoading(state)}
|
|
||||||
color={StateColor(state)}
|
|
||||||
>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</Stack>
|
|
||||||
</TabPanel>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Gotify() {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
const [gotifyData, setGotifyData] = useState<GotifyRecord>({});
|
|
||||||
const [loadingState, setLoadingState] = useState<LoadingState>(
|
|
||||||
LoadingState.none,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Add = () => {
|
|
||||||
setLoadingState(LoadingState.loading);
|
|
||||||
Api.notificationConnectorGotifyUpdate(gotifyData)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) setLoadingState(LoadingState.success);
|
|
||||||
else setLoadingState(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch((_) => setLoadingState(LoadingState.failure));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationConnectorTab value={"gotify"} add={Add} state={loadingState}>
|
|
||||||
<Input
|
|
||||||
placeholder={"Name"}
|
|
||||||
value={gotifyData.name as string}
|
|
||||||
onChange={(e) => setGotifyData({ ...gotifyData, name: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"https://[...]/message"}
|
|
||||||
value={gotifyData.endpoint as string}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGotifyData({ ...gotifyData, endpoint: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"Apptoken"}
|
|
||||||
type={"password"}
|
|
||||||
value={gotifyData.appToken as string}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGotifyData({ ...gotifyData, appToken: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"Priority"}
|
|
||||||
type={"number"}
|
|
||||||
value={gotifyData.priority as number}
|
|
||||||
onChange={(e) =>
|
|
||||||
setGotifyData({ ...gotifyData, priority: e.target.valueAsNumber })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</NotificationConnectorTab>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Ntfy() {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
const [ntfyData, setNtfyData] = useState<NtfyRecord>({});
|
|
||||||
const [loadingState, setLoadingState] = useState<LoadingState>(
|
|
||||||
LoadingState.none,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Add = () => {
|
|
||||||
setLoadingState(LoadingState.loading);
|
|
||||||
Api.notificationConnectorNtfyUpdate(ntfyData)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) setLoadingState(LoadingState.success);
|
|
||||||
else setLoadingState(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch((_) => setLoadingState(LoadingState.failure));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationConnectorTab value={"ntfy"} add={Add} state={loadingState}>
|
|
||||||
<Input
|
|
||||||
placeholder={"Name"}
|
|
||||||
value={ntfyData.name as string}
|
|
||||||
onChange={(e) => setNtfyData({ ...ntfyData, name: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"Endpoint"}
|
|
||||||
value={ntfyData.endpoint as string}
|
|
||||||
onChange={(e) => setNtfyData({ ...ntfyData, endpoint: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"Topic"}
|
|
||||||
value={ntfyData.topic as string}
|
|
||||||
onChange={(e) => setNtfyData({ ...ntfyData, topic: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"Username"}
|
|
||||||
value={ntfyData.username as string}
|
|
||||||
onChange={(e) => setNtfyData({ ...ntfyData, username: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"Password"}
|
|
||||||
type={"password"}
|
|
||||||
value={ntfyData.password as string}
|
|
||||||
onChange={(e) => setNtfyData({ ...ntfyData, password: e.target.value })}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"Priority"}
|
|
||||||
type={"number"}
|
|
||||||
value={ntfyData.priority as number}
|
|
||||||
onChange={(e) =>
|
|
||||||
setNtfyData({ ...ntfyData, priority: e.target.valueAsNumber })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</NotificationConnectorTab>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function Pushover() {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
const [pushoverData, setPushoverData] = useState<PushoverRecord>({});
|
|
||||||
const [loadingState, setLoadingState] = useState<LoadingState>(
|
|
||||||
LoadingState.none,
|
|
||||||
);
|
|
||||||
|
|
||||||
const Add = () => {
|
|
||||||
setLoadingState(LoadingState.loading);
|
|
||||||
Api.notificationConnectorPushoverUpdate(pushoverData)
|
|
||||||
.then((response) => {
|
|
||||||
if (response.ok) setLoadingState(LoadingState.success);
|
|
||||||
else setLoadingState(LoadingState.failure);
|
|
||||||
})
|
|
||||||
.catch((_) => setLoadingState(LoadingState.failure));
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<NotificationConnectorTab value={"pushover"} add={Add} state={loadingState}>
|
|
||||||
<Input
|
|
||||||
placeholder={"Name"}
|
|
||||||
value={pushoverData.name as string}
|
|
||||||
onChange={(e) =>
|
|
||||||
setPushoverData({ ...pushoverData, name: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"User"}
|
|
||||||
value={pushoverData.user as string}
|
|
||||||
onChange={(e) =>
|
|
||||||
setPushoverData({ ...pushoverData, user: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<Input
|
|
||||||
placeholder={"AppToken"}
|
|
||||||
type={"password"}
|
|
||||||
value={pushoverData.appToken as string}
|
|
||||||
onChange={(e) =>
|
|
||||||
setPushoverData({ ...pushoverData, appToken: e.target.value })
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</NotificationConnectorTab>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,67 +0,0 @@
|
|||||||
import { ApiContext } from "../../../apiClient/ApiContext.tsx";
|
|
||||||
import { useContext, useEffect, useState } from "react";
|
|
||||||
import { NotificationConnector } from "../../../apiClient/data-contracts.ts";
|
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
Chip,
|
|
||||||
Input,
|
|
||||||
Stack,
|
|
||||||
Table,
|
|
||||||
Textarea,
|
|
||||||
Typography,
|
|
||||||
} from "@mui/joy";
|
|
||||||
|
|
||||||
export default function () {
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
const [notificationConnectors, setNotificationConnectors] = useState<
|
|
||||||
NotificationConnector[]
|
|
||||||
>([]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Api.notificationConnectorList().then((r) => {
|
|
||||||
if (r.ok) setNotificationConnectors(r.data);
|
|
||||||
});
|
|
||||||
}, [Api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Stack direction={"column"} spacing={1}>
|
|
||||||
{notificationConnectors.map((c) => (
|
|
||||||
<NotificationConnectorItem key={c.name} connector={c} />
|
|
||||||
))}
|
|
||||||
</Stack>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function NotificationConnectorItem({
|
|
||||||
connector,
|
|
||||||
}: {
|
|
||||||
connector: NotificationConnector;
|
|
||||||
}) {
|
|
||||||
return (
|
|
||||||
<Card>
|
|
||||||
<Typography left={"h2"}>{connector.name}</Typography>
|
|
||||||
<Input
|
|
||||||
disabled
|
|
||||||
startDecorator={<Chip>{connector.httpMethod}</Chip>}
|
|
||||||
value={connector.url}
|
|
||||||
/>
|
|
||||||
<Table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Header</th>
|
|
||||||
<th>Value</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{Object.entries(connector.headers).map((x) => (
|
|
||||||
<tr key={x[0]}>
|
|
||||||
<td>{x[0]}</td>
|
|
||||||
<td>{[x[1]]}</td>
|
|
||||||
</tr>
|
|
||||||
))}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
<Textarea disabled value={connector.body} />
|
|
||||||
</Card>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,12 +1,11 @@
|
|||||||
import { SettingsItem } from "./Settings.tsx";
|
import { SettingsItem } from './Settings.tsx'
|
||||||
import FlareSolverr from "./FlareSolverr.tsx";
|
import FlareSolverr from './FlareSolverr.tsx'
|
||||||
import LibraryConnectors from "./LibraryConnectors/LibraryConnectors.tsx";
|
import { ReactNode } from 'react'
|
||||||
|
|
||||||
export default function () {
|
export default function Services(): ReactNode {
|
||||||
return (
|
return (
|
||||||
<SettingsItem title={"Services"}>
|
<SettingsItem title={'Services'}>
|
||||||
<FlareSolverr />
|
<FlareSolverr />
|
||||||
<LibraryConnectors />
|
</SettingsItem>
|
||||||
</SettingsItem>
|
)
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
@@ -1,114 +1,107 @@
|
|||||||
import ModalClose from "@mui/joy/ModalClose";
|
import ModalClose from '@mui/joy/ModalClose'
|
||||||
import {
|
import {
|
||||||
Accordion,
|
Accordion,
|
||||||
AccordionDetails,
|
AccordionDetails,
|
||||||
AccordionGroup,
|
AccordionGroup,
|
||||||
AccordionSummary,
|
AccordionSummary,
|
||||||
Button,
|
Button,
|
||||||
ColorPaletteProp,
|
DialogContent,
|
||||||
DialogContent,
|
DialogTitle,
|
||||||
DialogTitle,
|
Modal,
|
||||||
Input,
|
ModalDialog,
|
||||||
Modal,
|
} from '@mui/joy'
|
||||||
ModalDialog,
|
import './Settings.css'
|
||||||
} from "@mui/joy";
|
import * as React from 'react'
|
||||||
import "./Settings.css";
|
|
||||||
import * as React from "react";
|
|
||||||
import {
|
import {
|
||||||
createContext,
|
createContext,
|
||||||
ReactNode,
|
ReactNode,
|
||||||
useContext,
|
useContext,
|
||||||
useEffect,
|
useEffect,
|
||||||
useState,
|
useState,
|
||||||
} from "react";
|
} from 'react'
|
||||||
import { TrangaSettings } from "../../apiClient/data-contracts.ts";
|
import { SxProps } from '@mui/joy/styles/types'
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
import ImageCompression from './ImageCompression.tsx'
|
||||||
import { SxProps } from "@mui/joy/styles/types";
|
import FlareSolverr from './FlareSolverr.tsx'
|
||||||
import ImageCompression from "./ImageCompression.tsx";
|
import DownloadLanguage from './DownloadLanguage.tsx'
|
||||||
import FlareSolverr from "./FlareSolverr.tsx";
|
import ChapterNamingScheme from './ChapterNamingScheme.tsx'
|
||||||
import DownloadLanguage from "./DownloadLanguage.tsx";
|
import Maintenance from './Maintenance.tsx'
|
||||||
import ChapterNamingScheme from "./ChapterNamingScheme.tsx";
|
import { ApiContext } from '../../contexts/ApiContext.tsx'
|
||||||
import Maintenance from "./Maintenance.tsx";
|
import { TrangaSettings } from '../../api/data-contracts.ts'
|
||||||
|
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)
|
||||||
|
|
||||||
const [apiUriColor, setApiUriColor] = useState<ColorPaletteProp>("neutral");
|
useEffect(() => {
|
||||||
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
|
Api.settingsList().then((response) => {
|
||||||
|
setSettings(response.data)
|
||||||
|
})
|
||||||
|
}, [Api])
|
||||||
|
|
||||||
useEffect(() => {
|
const apiUriChanged = (
|
||||||
Api.settingsList().then((response) => {
|
value: string | number | readonly string[] | undefined
|
||||||
setSettings(response.data);
|
) => {
|
||||||
});
|
if (typeof value != 'string') return Promise.reject()
|
||||||
}, [Api]);
|
setApiUri(value)
|
||||||
|
return Promise.resolve()
|
||||||
|
}
|
||||||
|
|
||||||
const apiUriChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
|
const ModalStyle: SxProps = {
|
||||||
clearTimeout(timerRef.current);
|
width: '80%',
|
||||||
setApiUriColor("warning");
|
height: '80%',
|
||||||
timerRef.current = setTimeout(() => {
|
}
|
||||||
setApiUri(e.target.value);
|
|
||||||
setApiUriColor("success");
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
|
|
||||||
const ModalStyle: SxProps = {
|
return (
|
||||||
width: "80%",
|
<SettingsContext.Provider value={settings}>
|
||||||
height: "80%",
|
<Button onClick={() => setOpen(true)}>Settings</Button>
|
||||||
};
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog sx={ModalStyle}>
|
||||||
return (
|
<ModalClose />
|
||||||
<SettingsContext.Provider value={settings}>
|
<DialogTitle>Settings</DialogTitle>
|
||||||
<Button onClick={() => setOpen(true)}>Settings</Button>
|
<DialogContent>
|
||||||
<Modal open={open} onClose={() => setOpen(false)}>
|
<AccordionGroup>
|
||||||
<ModalDialog sx={ModalStyle}>
|
<SettingsItem title={'ApiUri'}>
|
||||||
<ModalClose />
|
<TInput
|
||||||
<DialogTitle>Settings</DialogTitle>
|
placeholder={'http(s)://'}
|
||||||
<DialogContent>
|
defaultValue={Api.baseUrl}
|
||||||
<AccordionGroup>
|
completionAction={apiUriChanged}
|
||||||
<SettingsItem title={"ApiUri"}>
|
/>
|
||||||
<Input
|
</SettingsItem>
|
||||||
color={apiUriColor}
|
<ImageCompression />
|
||||||
placeholder={"http(s)://"}
|
<FlareSolverr />
|
||||||
type={"url"}
|
<DownloadLanguage />
|
||||||
defaultValue={Api.baseUrl}
|
<ChapterNamingScheme />
|
||||||
onChange={apiUriChanged}
|
<Maintenance />
|
||||||
/>
|
</AccordionGroup>
|
||||||
</SettingsItem>
|
</DialogContent>
|
||||||
<ImageCompression />
|
</ModalDialog>
|
||||||
<FlareSolverr />
|
</Modal>
|
||||||
<DownloadLanguage />
|
</SettingsContext.Provider>
|
||||||
<ChapterNamingScheme />
|
)
|
||||||
<Maintenance />
|
|
||||||
</AccordionGroup>
|
|
||||||
</DialogContent>
|
|
||||||
</ModalDialog>
|
|
||||||
</Modal>
|
|
||||||
</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>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
@@ -1,75 +0,0 @@
|
|||||||
import { Dispatch, ReactNode, useContext, useEffect, useState } from "react";
|
|
||||||
import Drawer from "@mui/joy/Drawer";
|
|
||||||
import { Button, Option, Select, Table } from "@mui/joy";
|
|
||||||
import { BaseWorker } from "../../apiClient/data-contracts.ts";
|
|
||||||
import ModalClose from "@mui/joy/ModalClose";
|
|
||||||
import { ApiContext } from "../../apiClient/ApiContext.tsx";
|
|
||||||
|
|
||||||
export default function (): ReactNode {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
|
|
||||||
const [workers, setWorkers] = useState<BaseWorker[]>([]);
|
|
||||||
const Api = useContext(ApiContext);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Api.workerList().then((response) => {
|
|
||||||
if (response.ok) {
|
|
||||||
setWorkers(response.data);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [Api]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Button onClick={() => setOpen(true)}>Workers</Button>
|
|
||||||
<WorkerDrawer open={open} setOpen={setOpen} workers={workers} />
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function WorkerDrawer({
|
|
||||||
open,
|
|
||||||
setOpen,
|
|
||||||
workers,
|
|
||||||
}: {
|
|
||||||
open: boolean;
|
|
||||||
setOpen: Dispatch<boolean>;
|
|
||||||
workers: BaseWorker[];
|
|
||||||
}): ReactNode {
|
|
||||||
return (
|
|
||||||
<Drawer open={open} onClose={() => setOpen(false)} size={"lg"}>
|
|
||||||
<ModalClose />
|
|
||||||
<Table
|
|
||||||
borderAxis="bothBetween"
|
|
||||||
size="md"
|
|
||||||
stickyFooter={false}
|
|
||||||
stickyHeader
|
|
||||||
>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>Key</th>
|
|
||||||
<th>Can run</th>
|
|
||||||
<th>Missing dependencies</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody>
|
|
||||||
{workers.map((worker) => {
|
|
||||||
return (
|
|
||||||
<tr key={worker.key}>
|
|
||||||
<td>{worker.key}</td>
|
|
||||||
<td>{worker.allDependenciesFulfilled ? "yes" : "no"}</td>
|
|
||||||
<td>
|
|
||||||
<Select placeholder={"Missing dependencies"}>
|
|
||||||
{worker.missingDependencies?.map((worker) => {
|
|
||||||
return <Option value={worker.key}>{worker.key}</Option>;
|
|
||||||
})}
|
|
||||||
</Select>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</Drawer>
|
|
||||||
);
|
|
||||||
}
|
|
@@ -1,11 +1,11 @@
|
|||||||
.header {
|
.header {
|
||||||
position: sticky !important;
|
position: sticky !important;
|
||||||
z-index: 1000;
|
z-index: 1000;
|
||||||
top: 0;
|
top: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 60px;
|
height: 60px;
|
||||||
padding: 10px;
|
padding: 10px;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-flow: row nowrap;
|
flex-flow: row nowrap;
|
||||||
}
|
}
|
||||||
|
@@ -1,91 +1,99 @@
|
|||||||
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 "./apiClient/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'}>
|
||||||
<Stack
|
<Stack
|
||||||
direction={"row"}
|
direction={'row'}
|
||||||
spacing={2}
|
spacing={2}
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: '100%',
|
||||||
alignItems: "center",
|
alignItems: 'center',
|
||||||
justifyContent: "space-between",
|
justifyContent: 'space-between',
|
||||||
}}
|
}}
|
||||||
useFlexGap
|
useFlexGap
|
||||||
>
|
>
|
||||||
<Stack sx={{ flexGrow: 1, flexBasis: 1 }} direction={"row"} spacing={2}>
|
<Stack
|
||||||
{children}
|
sx={{ flexGrow: 1, flexBasis: 1 }}
|
||||||
</Stack>
|
direction={'row'}
|
||||||
<Stack
|
spacing={2}
|
||||||
sx={{
|
>
|
||||||
flexGrow: 1,
|
{children}
|
||||||
height: "100%",
|
</Stack>
|
||||||
flexBasis: 1,
|
<Stack
|
||||||
justifyContent: "center",
|
sx={{
|
||||||
}}
|
flexGrow: 1,
|
||||||
direction={"row"}
|
height: '100%',
|
||||||
>
|
flexBasis: 1,
|
||||||
<img
|
justifyContent: 'center',
|
||||||
src={"/blahaj.png"}
|
}}
|
||||||
style={{ cursor: "grab", maxHeight: "100%" }}
|
direction={'row'}
|
||||||
/>
|
>
|
||||||
<Typography
|
<img
|
||||||
level={"h2"}
|
src={'/blahaj.png'}
|
||||||
sx={{
|
style={{ cursor: 'grab', maxHeight: '100%' }}
|
||||||
background:
|
/>
|
||||||
"linear-gradient(110deg, var(--joy-palette-primary-solidBg), var(--joy-palette-success-400))",
|
<Typography
|
||||||
WebkitBackgroundClip: "text",
|
level={'h2'}
|
||||||
WebkitTextFillColor: "transparent",
|
sx={{
|
||||||
fontWeight: "bold",
|
background:
|
||||||
cursor: "default",
|
'linear-gradient(110deg, var(--joy-palette-primary-solidBg), var(--joy-palette-success-400))',
|
||||||
}}
|
WebkitBackgroundClip: 'text',
|
||||||
>
|
WebkitTextFillColor: 'transparent',
|
||||||
Tranga
|
fontWeight: 'bold',
|
||||||
</Typography>
|
cursor: 'default',
|
||||||
</Stack>
|
}}
|
||||||
<Stack
|
>
|
||||||
sx={{ flexGrow: 1, flexBasis: 1, justifyContent: "flex-end" }}
|
Tranga
|
||||||
direction={"row"}
|
</Typography>
|
||||||
spacing={2}
|
</Stack>
|
||||||
>
|
<Stack
|
||||||
<Link
|
sx={{
|
||||||
target={"_blank"}
|
flexGrow: 1,
|
||||||
href={"https://github.com/C9Glax/tranga"}
|
flexBasis: 1,
|
||||||
color={"neutral"}
|
justifyContent: 'flex-end',
|
||||||
height={"min-content"}
|
}}
|
||||||
>
|
direction={'row'}
|
||||||
<GitHub /> Server
|
spacing={2}
|
||||||
</Link>
|
>
|
||||||
<Link
|
<Link
|
||||||
target={"_blank"}
|
target={'_blank'}
|
||||||
href={"https://github.com/C9Glax/tranga-website"}
|
href={'https://github.com/C9Glax/tranga'}
|
||||||
color={"neutral"}
|
color={'neutral'}
|
||||||
height={"min-content"}
|
height={'min-content'}
|
||||||
>
|
>
|
||||||
<GitHub /> Website
|
<GitHub /> Server
|
||||||
</Link>
|
</Link>
|
||||||
<Link
|
<Link
|
||||||
target={"_blank"}
|
target={'_blank'}
|
||||||
href={Api.baseUrl + "/swagger"}
|
href={'https://github.com/C9Glax/tranga-website'}
|
||||||
color={"neutral"}
|
color={'neutral'}
|
||||||
height={"min-content"}
|
height={'min-content'}
|
||||||
>
|
>
|
||||||
<Article />
|
<GitHub /> Website
|
||||||
Swagger
|
</Link>
|
||||||
</Link>
|
<Link
|
||||||
</Stack>
|
target={'_blank'}
|
||||||
</Stack>
|
href={Api.baseUrl + '/swagger'}
|
||||||
</Sheet>
|
color={'neutral'}
|
||||||
);
|
height={'min-content'}
|
||||||
|
>
|
||||||
|
<Article />
|
||||||
|
Swagger
|
||||||
|
</Link>
|
||||||
|
</Stack>
|
||||||
|
</Stack>
|
||||||
|
</Sheet>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
123
tranga-website/src/Search.tsx
Normal file
123
tranga-website/src/Search.tsx
Normal file
@@ -0,0 +1,123 @@
|
|||||||
|
import { Dispatch, ReactNode, useContext, useState } from 'react'
|
||||||
|
import {
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
ListItemDecorator,
|
||||||
|
Modal,
|
||||||
|
ModalDialog,
|
||||||
|
Step,
|
||||||
|
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'
|
||||||
|
|
||||||
|
export default function Search({
|
||||||
|
open,
|
||||||
|
setOpen,
|
||||||
|
}: {
|
||||||
|
open: boolean
|
||||||
|
setOpen: Dispatch<boolean>
|
||||||
|
}): ReactNode {
|
||||||
|
const Api = useContext(ApiContext)
|
||||||
|
const MangaConnectors = useContext(MangaConnectorContext)
|
||||||
|
|
||||||
|
const [selectedConnector, setSelectedConnector] = useState<MangaConnector>()
|
||||||
|
|
||||||
|
const [searchResults, setSearchResults] = useState<MinimalManga[]>([])
|
||||||
|
|
||||||
|
const startSearch = (
|
||||||
|
value: string | number | readonly string[] | undefined
|
||||||
|
): Promise<void> => {
|
||||||
|
if (typeof value != 'string') return Promise.reject()
|
||||||
|
setSearchResults([])
|
||||||
|
if (isUrl(value)) {
|
||||||
|
return Api.searchUrlCreate(value)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.ok) {
|
||||||
|
setSearchResults([result.data])
|
||||||
|
return Promise.resolve()
|
||||||
|
} else return Promise.reject()
|
||||||
|
})
|
||||||
|
.catch(Promise.reject)
|
||||||
|
} else {
|
||||||
|
if (!selectedConnector) return Promise.reject()
|
||||||
|
return Api.searchDetail(selectedConnector?.key, value)
|
||||||
|
.then((result) => {
|
||||||
|
if (result.ok) {
|
||||||
|
setSearchResults(result.data)
|
||||||
|
return Promise.resolve()
|
||||||
|
} else return Promise.reject()
|
||||||
|
})
|
||||||
|
.catch(Promise.reject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal open={open} onClose={() => setOpen(false)}>
|
||||||
|
<ModalDialog sx={{ width: '90vw' }}>
|
||||||
|
<ModalClose />
|
||||||
|
<Stepper>
|
||||||
|
<Step
|
||||||
|
orientation={'vertical'}
|
||||||
|
indicator={<StepIndicator>1</StepIndicator>}
|
||||||
|
>
|
||||||
|
<Typography level={'title-lg'}>
|
||||||
|
Select a connector
|
||||||
|
</Typography>
|
||||||
|
<List>
|
||||||
|
{MangaConnectors.map((c) => (
|
||||||
|
<ListItem
|
||||||
|
onClick={() => setSelectedConnector(c)}
|
||||||
|
>
|
||||||
|
<ListItemDecorator>
|
||||||
|
<MangaConnectorIcon
|
||||||
|
mangaConnector={c}
|
||||||
|
/>
|
||||||
|
</ListItemDecorator>
|
||||||
|
<Typography
|
||||||
|
sx={
|
||||||
|
c.key == selectedConnector?.key
|
||||||
|
? { fontWeight: 'bold' }
|
||||||
|
: {}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{c.name}
|
||||||
|
</Typography>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
</Step>
|
||||||
|
<Step
|
||||||
|
orientation={'vertical'}
|
||||||
|
indicator={<StepIndicator>2</StepIndicator>}
|
||||||
|
>
|
||||||
|
<Typography level={'title-lg'}>
|
||||||
|
Enter a search term or URL
|
||||||
|
</Typography>
|
||||||
|
<TInput
|
||||||
|
placeholder={'Manga-name or URL'}
|
||||||
|
completionAction={startSearch}
|
||||||
|
/>
|
||||||
|
</Step>
|
||||||
|
</Stepper>
|
||||||
|
<MangaCardList manga={searchResults} />
|
||||||
|
</ModalDialog>
|
||||||
|
</Modal>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isUrl(str: string): boolean {
|
||||||
|
try {
|
||||||
|
new URL(str)
|
||||||
|
return true
|
||||||
|
} catch {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
@@ -1,4 +0,0 @@
|
|||||||
import { createContext } from "react";
|
|
||||||
import { V2 } from "./V2.ts";
|
|
||||||
|
|
||||||
export const ApiContext = createContext<V2>(new V2());
|
|
@@ -1,32 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* tslint:disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
/*
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
|
||||||
* ## ##
|
|
||||||
* ## AUTHOR: acacode ##
|
|
||||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
import { HttpClient, RequestParams } from "./http-client";
|
|
||||||
|
|
||||||
export class CleanupNoDownloadManga<
|
|
||||||
SecurityDataType = unknown,
|
|
||||||
> extends HttpClient<SecurityDataType> {
|
|
||||||
/**
|
|
||||||
* No description
|
|
||||||
*
|
|
||||||
* @tags Maintenance
|
|
||||||
* @name CleanupNoDownloadMangaCreate
|
|
||||||
* @summary Removes all API.Schema.MangaContext.Manga not marked for Download on any API.MangaConnectors.MangaConnector
|
|
||||||
* @request POST:/CleanupNoDownloadManga
|
|
||||||
*/
|
|
||||||
cleanupNoDownloadMangaCreate = (params: RequestParams = {}) =>
|
|
||||||
this.request<void, string>({
|
|
||||||
path: `/CleanupNoDownloadManga`,
|
|
||||||
method: "POST",
|
|
||||||
...params,
|
|
||||||
});
|
|
||||||
}
|
|
@@ -1,31 +0,0 @@
|
|||||||
import { createContext, useContext, useState } from "react";
|
|
||||||
import { TrangaSettings } from "./data-contracts.ts";
|
|
||||||
import { ApiContext } from "./ApiContext.tsx";
|
|
||||||
|
|
||||||
const [settingsPromise, setSettingsPromise] =
|
|
||||||
useState<Promise<TrangaSettings | undefined>>();
|
|
||||||
const [settings, setSettings] = useState<TrangaSettings>();
|
|
||||||
|
|
||||||
export const SettingsContext = createContext<{
|
|
||||||
GetSettings: () => Promise<TrangaSettings | undefined>;
|
|
||||||
}>({
|
|
||||||
GetSettings: (): Promise<TrangaSettings | undefined> => {
|
|
||||||
const API = useContext(ApiContext);
|
|
||||||
const promise = settingsPromise;
|
|
||||||
if (promise) return promise;
|
|
||||||
const p = new Promise<TrangaSettings | undefined>((resolve, reject) => {
|
|
||||||
if (settings) resolve(settings);
|
|
||||||
|
|
||||||
console.log(`Fetching settings`);
|
|
||||||
API.settingsList()
|
|
||||||
.then((result) => {
|
|
||||||
if (!result.ok) throw new Error(`Error fetching settings`);
|
|
||||||
setSettings(result.data);
|
|
||||||
resolve(result.data);
|
|
||||||
})
|
|
||||||
.catch(reject);
|
|
||||||
});
|
|
||||||
setSettingsPromise(p);
|
|
||||||
return p;
|
|
||||||
},
|
|
||||||
});
|
|
File diff suppressed because it is too large
Load Diff
@@ -1,438 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* tslint:disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
/*
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
|
||||||
* ## ##
|
|
||||||
* ## AUTHOR: acacode ##
|
|
||||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
export enum WorkerExecutionState {
|
|
||||||
Failed = "Failed",
|
|
||||||
Cancelled = "Cancelled",
|
|
||||||
Created = "Created",
|
|
||||||
Waiting = "Waiting",
|
|
||||||
Running = "Running",
|
|
||||||
Completed = "Completed",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum RequestType {
|
|
||||||
Default = "Default",
|
|
||||||
MangaDexFeed = "MangaDexFeed",
|
|
||||||
MangaImage = "MangaImage",
|
|
||||||
MangaCover = "MangaCover",
|
|
||||||
MangaDexImage = "MangaDexImage",
|
|
||||||
MangaInfo = "MangaInfo",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum MangaReleaseStatus {
|
|
||||||
Continuing = "Continuing",
|
|
||||||
Completed = "Completed",
|
|
||||||
OnHiatus = "OnHiatus",
|
|
||||||
Cancelled = "Cancelled",
|
|
||||||
Unreleased = "Unreleased",
|
|
||||||
}
|
|
||||||
|
|
||||||
export enum LibraryType {
|
|
||||||
Komga = "Komga",
|
|
||||||
Kavita = "Kavita",
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AltTitle {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 8
|
|
||||||
*/
|
|
||||||
language: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
title: string;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Author {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 128
|
|
||||||
*/
|
|
||||||
authorName: string;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface BaseWorker {
|
|
||||||
/** Workers this Worker depends on being completed before running. */
|
|
||||||
dependsOn?: BaseWorker[] | null;
|
|
||||||
/** API.Workers.BaseWorker.DependsOn where API.Workers.WorkerExecutionState is less than Completed. */
|
|
||||||
missingDependencies?: BaseWorker[] | null;
|
|
||||||
allDependenciesFulfilled?: boolean;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Chapter {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
parentMangaId: string;
|
|
||||||
idsOnMangaConnectors?: Record<string, string>;
|
|
||||||
/** @format int32 */
|
|
||||||
volumeNumber?: number | null;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 10
|
|
||||||
*/
|
|
||||||
chapterNumber: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
title?: string | null;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
fileName: string;
|
|
||||||
downloaded: boolean;
|
|
||||||
fullArchiveFilePath?: string | null;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ChapterMangaConnectorId {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
objId: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 32
|
|
||||||
*/
|
|
||||||
mangaConnectorName: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
idOnConnectorSite: string;
|
|
||||||
/**
|
|
||||||
* @format uri
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 512
|
|
||||||
*/
|
|
||||||
websiteUrl?: string | null;
|
|
||||||
useForDownload?: boolean;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface FileLibrary {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
basePath: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 512
|
|
||||||
*/
|
|
||||||
libraryName: string;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface GotifyRecord {
|
|
||||||
name?: string | null;
|
|
||||||
endpoint?: string | null;
|
|
||||||
appToken?: string | null;
|
|
||||||
/** @format int32 */
|
|
||||||
priority?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LibraryConnector {
|
|
||||||
libraryType: LibraryType;
|
|
||||||
/**
|
|
||||||
* @format uri
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
baseUrl: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
auth: string;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Link {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
linkProvider: string;
|
|
||||||
/**
|
|
||||||
* @format uri
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 2048
|
|
||||||
*/
|
|
||||||
linkUrl: string;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Manga {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 512
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/** @minLength 1 */
|
|
||||||
description: string;
|
|
||||||
releaseStatus: MangaReleaseStatus;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
libraryId?: string | null;
|
|
||||||
authors?: Author[] | null;
|
|
||||||
mangaTags?: MangaTag[] | null;
|
|
||||||
links?: Link[] | null;
|
|
||||||
altTitles?: AltTitle[] | null;
|
|
||||||
/** @format float */
|
|
||||||
ignoreChaptersBefore: number;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 1024
|
|
||||||
*/
|
|
||||||
directoryName: string;
|
|
||||||
/** @format int32 */
|
|
||||||
year?: number | null;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 8
|
|
||||||
*/
|
|
||||||
originalLanguage?: string | null;
|
|
||||||
chapterIds?: string[] | null;
|
|
||||||
idsOnMangaConnectors?: Record<string, string>;
|
|
||||||
mangaConnectorIdsIds?: string[] | null;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MangaConnector {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 32
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 8
|
|
||||||
*/
|
|
||||||
supportedLanguages: string[];
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 2048
|
|
||||||
*/
|
|
||||||
iconUrl: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
baseUris: string[];
|
|
||||||
enabled: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MangaMangaConnectorId {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
objId: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 32
|
|
||||||
*/
|
|
||||||
mangaConnectorName: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 256
|
|
||||||
*/
|
|
||||||
idOnConnectorSite: string;
|
|
||||||
/**
|
|
||||||
* @format uri
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 512
|
|
||||||
*/
|
|
||||||
websiteUrl?: string | null;
|
|
||||||
useForDownload?: boolean;
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MangaTag {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
tag: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MetadataEntry {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface MinimalManga {
|
|
||||||
/**
|
|
||||||
* @minLength 16
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
key: string;
|
|
||||||
/** @minLength 1 */
|
|
||||||
name: string;
|
|
||||||
/** @minLength 1 */
|
|
||||||
description: string;
|
|
||||||
releaseStatus: MangaReleaseStatus;
|
|
||||||
mangaConnectorIds?: MangaMangaConnectorId[] | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NotificationConnector {
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 64
|
|
||||||
*/
|
|
||||||
name: string;
|
|
||||||
/**
|
|
||||||
* @format uri
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 2048
|
|
||||||
*/
|
|
||||||
url: string;
|
|
||||||
headers: Record<string, string>;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 8
|
|
||||||
*/
|
|
||||||
httpMethod: string;
|
|
||||||
/**
|
|
||||||
* @minLength 0
|
|
||||||
* @maxLength 4096
|
|
||||||
*/
|
|
||||||
body: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface NtfyRecord {
|
|
||||||
name?: string | null;
|
|
||||||
endpoint?: string | null;
|
|
||||||
username?: string | null;
|
|
||||||
password?: string | null;
|
|
||||||
topic?: string | null;
|
|
||||||
/** @format int32 */
|
|
||||||
priority?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ProblemDetails {
|
|
||||||
type?: string | null;
|
|
||||||
title?: string | null;
|
|
||||||
/** @format int32 */
|
|
||||||
status?: number | null;
|
|
||||||
detail?: string | null;
|
|
||||||
instance?: string | null;
|
|
||||||
[key: string]: any;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface PushoverRecord {
|
|
||||||
name?: string | null;
|
|
||||||
appToken?: string | null;
|
|
||||||
user?: string | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface TrangaSettings {
|
|
||||||
downloadLocation?: string | null;
|
|
||||||
userAgent?: string | null;
|
|
||||||
/** @format int32 */
|
|
||||||
imageCompression?: number;
|
|
||||||
blackWhiteImages?: boolean;
|
|
||||||
flareSolverrUrl?: string | null;
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
*/
|
|
||||||
chapterNamingScheme?: string | null;
|
|
||||||
/** @format int32 */
|
|
||||||
workCycleTimeoutMs?: number;
|
|
||||||
requestLimits?: {
|
|
||||||
/** @format int32 */
|
|
||||||
Default?: number;
|
|
||||||
/** @format int32 */
|
|
||||||
MangaDexFeed?: number;
|
|
||||||
/** @format int32 */
|
|
||||||
MangaImage?: number;
|
|
||||||
/** @format int32 */
|
|
||||||
MangaCover?: number;
|
|
||||||
/** @format int32 */
|
|
||||||
MangaDexImage?: number;
|
|
||||||
/** @format int32 */
|
|
||||||
MangaInfo?: number;
|
|
||||||
} | null;
|
|
||||||
downloadLanguage?: string | null;
|
|
||||||
}
|
|
@@ -1,260 +0,0 @@
|
|||||||
/* eslint-disable */
|
|
||||||
/* tslint:disable */
|
|
||||||
// @ts-nocheck
|
|
||||||
/*
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
* ## THIS FILE WAS GENERATED VIA SWAGGER-TYPESCRIPT-API ##
|
|
||||||
* ## ##
|
|
||||||
* ## AUTHOR: acacode ##
|
|
||||||
* ## SOURCE: https://github.com/acacode/swagger-typescript-api ##
|
|
||||||
* ---------------------------------------------------------------
|
|
||||||
*/
|
|
||||||
|
|
||||||
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;
|
|
||||||
/** request path */
|
|
||||||
path: string;
|
|
||||||
/** content type of request body */
|
|
||||||
type?: ContentType;
|
|
||||||
/** query params */
|
|
||||||
query?: QueryParamsType;
|
|
||||||
/** format of response (i.e. response.json() -> format: "json") */
|
|
||||||
format?: ResponseFormat;
|
|
||||||
/** request body */
|
|
||||||
body?: unknown;
|
|
||||||
/** base url */
|
|
||||||
baseUrl?: string;
|
|
||||||
/** request cancellation token */
|
|
||||||
cancelToken?: CancelToken;
|
|
||||||
}
|
|
||||||
|
|
||||||
export type RequestParams = Omit<
|
|
||||||
FullRequestParams,
|
|
||||||
"body" | "method" | "query" | "path"
|
|
||||||
>;
|
|
||||||
|
|
||||||
export interface ApiConfig<SecurityDataType = unknown> {
|
|
||||||
baseUrl?: string;
|
|
||||||
baseApiParams?: Omit<RequestParams, "baseUrl" | "cancelToken" | "signal">;
|
|
||||||
securityWorker?: (
|
|
||||||
securityData: SecurityDataType | null,
|
|
||||||
) => Promise<RequestParams | void> | RequestParams | void;
|
|
||||||
customFetch?: typeof fetch;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface HttpResponse<D extends unknown, E extends unknown = unknown>
|
|
||||||
extends Response {
|
|
||||||
data: D;
|
|
||||||
error: E;
|
|
||||||
}
|
|
||||||
|
|
||||||
type CancelToken = Symbol | string | number;
|
|
||||||
|
|
||||||
export enum ContentType {
|
|
||||||
Json = "application/json",
|
|
||||||
JsonApi = "application/vnd.api+json",
|
|
||||||
FormData = "multipart/form-data",
|
|
||||||
UrlEncoded = "application/x-www-form-urlencoded",
|
|
||||||
Text = "text/plain",
|
|
||||||
}
|
|
||||||
|
|
||||||
export class HttpClient<SecurityDataType = unknown> {
|
|
||||||
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);
|
|
||||||
|
|
||||||
private baseApiParams: RequestParams = {
|
|
||||||
credentials: "same-origin",
|
|
||||||
headers: {},
|
|
||||||
redirect: "follow",
|
|
||||||
referrerPolicy: "no-referrer",
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(apiConfig: ApiConfig<SecurityDataType> = {}) {
|
|
||||||
Object.assign(this, apiConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
public setSecurityData = (data: SecurityDataType | null) => {
|
|
||||||
this.securityData = data;
|
|
||||||
};
|
|
||||||
|
|
||||||
protected encodeQueryParam(key: string, value: any) {
|
|
||||||
const encodedKey = encodeURIComponent(key);
|
|
||||||
return `${encodedKey}=${encodeURIComponent(typeof value === "number" ? value : `${value}`)}`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addQueryParam(query: QueryParamsType, key: string) {
|
|
||||||
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("&");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected toQueryString(rawQuery?: QueryParamsType): string {
|
|
||||||
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("&");
|
|
||||||
}
|
|
||||||
|
|
||||||
protected addQueryParams(rawQuery?: QueryParamsType): string {
|
|
||||||
const queryString = this.toQueryString(rawQuery);
|
|
||||||
return queryString ? `?${queryString}` : "";
|
|
||||||
}
|
|
||||||
|
|
||||||
private contentFormatters: Record<ContentType, (input: any) => any> = {
|
|
||||||
[ContentType.Json]: (input: any) =>
|
|
||||||
input !== null && (typeof input === "object" || typeof input === "string")
|
|
||||||
? JSON.stringify(input)
|
|
||||||
: input,
|
|
||||||
[ContentType.JsonApi]: (input: any) =>
|
|
||||||
input !== null && (typeof input === "object" || typeof input === "string")
|
|
||||||
? JSON.stringify(input)
|
|
||||||
: input,
|
|
||||||
[ContentType.Text]: (input: any) =>
|
|
||||||
input !== null && typeof input !== "string"
|
|
||||||
? JSON.stringify(input)
|
|
||||||
: input,
|
|
||||||
[ContentType.FormData]: (input: any) =>
|
|
||||||
Object.keys(input || {}).reduce((formData, key) => {
|
|
||||||
const property = input[key];
|
|
||||||
formData.append(
|
|
||||||
key,
|
|
||||||
property instanceof Blob
|
|
||||||
? property
|
|
||||||
: typeof property === "object" && property !== null
|
|
||||||
? JSON.stringify(property)
|
|
||||||
: `${property}`,
|
|
||||||
);
|
|
||||||
return formData;
|
|
||||||
}, new FormData()),
|
|
||||||
[ContentType.UrlEncoded]: (input: any) => this.toQueryString(input),
|
|
||||||
};
|
|
||||||
|
|
||||||
protected mergeRequestParams(
|
|
||||||
params1: RequestParams,
|
|
||||||
params2?: RequestParams,
|
|
||||||
): RequestParams {
|
|
||||||
return {
|
|
||||||
...this.baseApiParams,
|
|
||||||
...params1,
|
|
||||||
...(params2 || {}),
|
|
||||||
headers: {
|
|
||||||
...(this.baseApiParams.headers || {}),
|
|
||||||
...(params1.headers || {}),
|
|
||||||
...((params2 && params2.headers) || {}),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
protected createAbortSignal = (
|
|
||||||
cancelToken: CancelToken,
|
|
||||||
): AbortSignal | undefined => {
|
|
||||||
if (this.abortControllers.has(cancelToken)) {
|
|
||||||
const abortController = this.abortControllers.get(cancelToken);
|
|
||||||
if (abortController) {
|
|
||||||
return abortController.signal;
|
|
||||||
}
|
|
||||||
return void 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
const abortController = new AbortController();
|
|
||||||
this.abortControllers.set(cancelToken, abortController);
|
|
||||||
return abortController.signal;
|
|
||||||
};
|
|
||||||
|
|
||||||
public abortRequest = (cancelToken: CancelToken) => {
|
|
||||||
const abortController = this.abortControllers.get(cancelToken);
|
|
||||||
|
|
||||||
if (abortController) {
|
|
||||||
abortController.abort();
|
|
||||||
this.abortControllers.delete(cancelToken);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public request = async <T = any, E = any>({
|
|
||||||
body,
|
|
||||||
secure,
|
|
||||||
path,
|
|
||||||
type,
|
|
||||||
query,
|
|
||||||
format,
|
|
||||||
baseUrl,
|
|
||||||
cancelToken,
|
|
||||||
...params
|
|
||||||
}: FullRequestParams): Promise<HttpResponse<T, E>> => {
|
|
||||||
const secureParams =
|
|
||||||
((typeof secure === "boolean" ? secure : this.baseApiParams.secure) &&
|
|
||||||
this.securityWorker &&
|
|
||||||
(await this.securityWorker(this.securityData))) ||
|
|
||||||
{};
|
|
||||||
const requestParams = this.mergeRequestParams(params, secureParams);
|
|
||||||
const queryString = query && this.toQueryString(query);
|
|
||||||
const payloadFormatter = this.contentFormatters[type || ContentType.Json];
|
|
||||||
const responseFormat = format || requestParams.format;
|
|
||||||
|
|
||||||
return this.customFetch(
|
|
||||||
`${baseUrl || this.baseUrl || ""}${path}${queryString ? `?${queryString}` : ""}`,
|
|
||||||
{
|
|
||||||
...requestParams,
|
|
||||||
headers: {
|
|
||||||
...(requestParams.headers || {}),
|
|
||||||
...(type && type !== ContentType.FormData
|
|
||||||
? { "Content-Type": type }
|
|
||||||
: {}),
|
|
||||||
},
|
|
||||||
signal:
|
|
||||||
(cancelToken
|
|
||||||
? this.createAbortSignal(cancelToken)
|
|
||||||
: requestParams.signal) || null,
|
|
||||||
body:
|
|
||||||
typeof body === "undefined" || body === null
|
|
||||||
? null
|
|
||||||
: 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 data = !responseFormat
|
|
||||||
? r
|
|
||||||
: await response[responseFormat]()
|
|
||||||
.then((data) => {
|
|
||||||
if (r.ok) {
|
|
||||||
r.data = data;
|
|
||||||
} else {
|
|
||||||
r.error = data;
|
|
||||||
}
|
|
||||||
return r;
|
|
||||||
})
|
|
||||||
.catch((e) => {
|
|
||||||
r.error = e;
|
|
||||||
return r;
|
|
||||||
});
|
|
||||||
|
|
||||||
if (cancelToken) {
|
|
||||||
this.abortControllers.delete(cancelToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!response.ok) throw data;
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
20
tranga-website/src/contexts/ApiContext.tsx
Normal file
20
tranga-website/src/contexts/ApiContext.tsx
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
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 default function ApiProvider({
|
||||||
|
apiConfig,
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
apiConfig: ApiConfig
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
const [api, setApi] = useState<V2>(new V2(apiConfig))
|
||||||
|
useEffect(() => {
|
||||||
|
setApi(new V2(apiConfig))
|
||||||
|
}, [apiConfig])
|
||||||
|
|
||||||
|
return <ApiContext value={api}>{children}</ApiContext>
|
||||||
|
}
|
33
tranga-website/src/contexts/MangaConnectorContext.tsx
Normal file
33
tranga-website/src/contexts/MangaConnectorContext.tsx
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
import {
|
||||||
|
createContext,
|
||||||
|
ReactNode,
|
||||||
|
useContext,
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
} from 'react'
|
||||||
|
import { MangaConnector } from '../api/data-contracts.ts'
|
||||||
|
import { ApiContext } from './ApiContext.tsx'
|
||||||
|
|
||||||
|
export const MangaConnectorContext = createContext<MangaConnector[]>([])
|
||||||
|
|
||||||
|
export default function MangaConnectorProvider({
|
||||||
|
children,
|
||||||
|
}: {
|
||||||
|
children: ReactNode
|
||||||
|
}) {
|
||||||
|
const Api = useContext(ApiContext)
|
||||||
|
|
||||||
|
const [state, setState] = useState<MangaConnector[]>([])
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Api.mangaConnectorList().then((result) => {
|
||||||
|
if (result.ok) {
|
||||||
|
setState(result.data)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}, [Api])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<MangaConnectorContext value={state}>{children}</MangaConnectorContext>
|
||||||
|
)
|
||||||
|
}
|
43
tranga-website/src/contexts/MangaContext.tsx
Normal file
43
tranga-website/src/contexts/MangaContext.tsx
Normal file
@@ -0,0 +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'
|
||||||
|
|
||||||
|
export const MangaContext = createContext<M>({
|
||||||
|
GetManga: () => Promise.reject(),
|
||||||
|
})
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 (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
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
return undefined
|
||||||
|
})
|
||||||
|
promises.set(key, newPromise)
|
||||||
|
return newPromise
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface M {
|
||||||
|
GetManga(key: string): Promise<Manga | undefined>
|
||||||
|
}
|
@@ -1,25 +1,25 @@
|
|||||||
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 (
|
||||||
<StrictMode>
|
<StrictMode>
|
||||||
<CssVarsProvider theme={trangaTheme}>
|
<CssVarsProvider theme={trangaTheme}>
|
||||||
{/* must be used under CssVarsProvider */}
|
{/* must be used under CssVarsProvider */}
|
||||||
<CssBaseline />
|
<CssBaseline />
|
||||||
|
|
||||||
{/* The rest of your application */}
|
{/* The rest of your application */}
|
||||||
<App />
|
<App />
|
||||||
</CssVarsProvider>
|
</CssVarsProvider>
|
||||||
</StrictMode>
|
</StrictMode>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
createRoot(document.getElementById("root")!).render(<MyApp />);
|
createRoot(document.getElementById('root')!).render(<MyApp />)
|
||||||
|
@@ -1,87 +1,87 @@
|
|||||||
import { extendTheme } from "@mui/joy/styles";
|
import { extendTheme } from '@mui/joy/styles'
|
||||||
|
|
||||||
export const trangaTheme = extendTheme({
|
export const trangaTheme = extendTheme({
|
||||||
colorSchemes: {
|
colorSchemes: {
|
||||||
light: {
|
light: {
|
||||||
palette: {
|
palette: {
|
||||||
primary: {
|
primary: {
|
||||||
"50": "#FCE5EA",
|
'50': '#FCE5EA',
|
||||||
"100": "#FBDDE3",
|
'100': '#FBDDE3',
|
||||||
"200": "#F9CBD4",
|
'200': '#F9CBD4',
|
||||||
"300": "#F7BAC6",
|
'300': '#F7BAC6',
|
||||||
"400": "#F5A9B8",
|
'400': '#F5A9B8',
|
||||||
"500": "#F5A9B8",
|
'500': '#F5A9B8',
|
||||||
"600": "#C48793",
|
'600': '#C48793',
|
||||||
"700": "#AC7681",
|
'700': '#AC7681',
|
||||||
"800": "#93656E",
|
'800': '#93656E',
|
||||||
"900": "#7B555C",
|
'900': '#7B555C',
|
||||||
|
},
|
||||||
|
neutral: {
|
||||||
|
'50': '#E6E6E6',
|
||||||
|
'100': '#CCCCCC',
|
||||||
|
'200': '#B3B3B3',
|
||||||
|
'300': '#999999',
|
||||||
|
'400': '#808080',
|
||||||
|
'500': '#666666',
|
||||||
|
'600': '#4C4C4C',
|
||||||
|
'700': '#333333',
|
||||||
|
'800': '#191919',
|
||||||
|
'900': '#000',
|
||||||
|
plainColor: 'var(--joy-palette-neutral-50)',
|
||||||
|
plainHoverBg: 'var(--joy-palette-neutral-700)',
|
||||||
|
outlinedColor: 'var(--joy-palette-neutral-50)',
|
||||||
|
},
|
||||||
|
success: {
|
||||||
|
'50': '#cef0fe',
|
||||||
|
'100': '#bdebfd',
|
||||||
|
'200': '#9de2fc',
|
||||||
|
'300': '#7cd8fb',
|
||||||
|
'400': '#5bcefa',
|
||||||
|
'500': '#5bcefa',
|
||||||
|
'600': '#49a5c8',
|
||||||
|
'700': '#4090af',
|
||||||
|
'800': '#2e677d',
|
||||||
|
'900': '#245264',
|
||||||
|
},
|
||||||
|
danger: {
|
||||||
|
'50': '#f2c0b3',
|
||||||
|
'100': '#ea9680',
|
||||||
|
'200': '#e68166',
|
||||||
|
'300': '#dd5733',
|
||||||
|
'400': '#d52d00',
|
||||||
|
'500': '#d52d00',
|
||||||
|
'600': '#aa2400',
|
||||||
|
'700': '#951f00',
|
||||||
|
'800': '#6b1700',
|
||||||
|
'900': '#400d00',
|
||||||
|
},
|
||||||
|
warning: {
|
||||||
|
'50': '#ffebdd',
|
||||||
|
'100': '#ffd7bb',
|
||||||
|
'200': '#ffc29a',
|
||||||
|
'300': '#ffae78',
|
||||||
|
'400': '#ff9a56',
|
||||||
|
'500': '#ff9a56',
|
||||||
|
'600': '#cc7b45',
|
||||||
|
'700': '#995c34',
|
||||||
|
'800': '#663e22',
|
||||||
|
'900': '#331f11',
|
||||||
|
},
|
||||||
|
background: {
|
||||||
|
body: 'var(--joy-palette-neutral-900)',
|
||||||
|
surface: 'var(--joy-palette-neutral-900)',
|
||||||
|
popup: 'var(--joy-palette-neutral-800)',
|
||||||
|
},
|
||||||
|
text: {
|
||||||
|
primary: 'var(--joy-palette-neutral-50)',
|
||||||
|
secondary: 'var(--joy-palette-success-200)',
|
||||||
|
tertiary: 'var(--joy-palette-primary-200)',
|
||||||
|
icon: 'var(--joy-palette-primary-50)',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
neutral: {
|
dark: {
|
||||||
"50": "#E6E6E6",
|
palette: {},
|
||||||
"100": "#CCCCCC",
|
|
||||||
"200": "#B3B3B3",
|
|
||||||
"300": "#999999",
|
|
||||||
"400": "#808080",
|
|
||||||
"500": "#666666",
|
|
||||||
"600": "#4C4C4C",
|
|
||||||
"700": "#333333",
|
|
||||||
"800": "#191919",
|
|
||||||
"900": "#000",
|
|
||||||
plainColor: "var(--joy-palette-neutral-50)",
|
|
||||||
plainHoverBg: "var(--joy-palette-neutral-700)",
|
|
||||||
outlinedColor: "var(--joy-palette-neutral-50)",
|
|
||||||
},
|
},
|
||||||
success: {
|
|
||||||
"50": "#cef0fe",
|
|
||||||
"100": "#bdebfd",
|
|
||||||
"200": "#9de2fc",
|
|
||||||
"300": "#7cd8fb",
|
|
||||||
"400": "#5bcefa",
|
|
||||||
"500": "#5bcefa",
|
|
||||||
"600": "#49a5c8",
|
|
||||||
"700": "#4090af",
|
|
||||||
"800": "#2e677d",
|
|
||||||
"900": "#245264",
|
|
||||||
},
|
|
||||||
danger: {
|
|
||||||
"50": "#f2c0b3",
|
|
||||||
"100": "#ea9680",
|
|
||||||
"200": "#e68166",
|
|
||||||
"300": "#dd5733",
|
|
||||||
"400": "#d52d00",
|
|
||||||
"500": "#d52d00",
|
|
||||||
"600": "#aa2400",
|
|
||||||
"700": "#951f00",
|
|
||||||
"800": "#6b1700",
|
|
||||||
"900": "#400d00",
|
|
||||||
},
|
|
||||||
warning: {
|
|
||||||
"50": "#ffebdd",
|
|
||||||
"100": "#ffd7bb",
|
|
||||||
"200": "#ffc29a",
|
|
||||||
"300": "#ffae78",
|
|
||||||
"400": "#ff9a56",
|
|
||||||
"500": "#ff9a56",
|
|
||||||
"600": "#cc7b45",
|
|
||||||
"700": "#995c34",
|
|
||||||
"800": "#663e22",
|
|
||||||
"900": "#331f11",
|
|
||||||
},
|
|
||||||
background: {
|
|
||||||
body: "var(--joy-palette-neutral-900)",
|
|
||||||
surface: "var(--joy-palette-neutral-900)",
|
|
||||||
popup: "var(--joy-palette-neutral-800)",
|
|
||||||
},
|
|
||||||
text: {
|
|
||||||
primary: "var(--joy-palette-neutral-50)",
|
|
||||||
secondary: "var(--joy-palette-success-200)",
|
|
||||||
tertiary: "var(--joy-palette-primary-200)",
|
|
||||||
icon: "var(--joy-palette-primary-50)",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
dark: {
|
})
|
||||||
palette: {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
@@ -1,26 +1,26 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
|
||||||
"target": "ES2020",
|
"target": "ES2020",
|
||||||
"useDefineForClassFields": true,
|
"useDefineForClassFields": true,
|
||||||
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
"lib": ["ES2020", "DOM", "DOM.Iterable"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
"jsx": "react-jsx",
|
"jsx": "react-jsx",
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["src"]
|
"include": ["src"]
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
{
|
{
|
||||||
"files": [],
|
"files": [],
|
||||||
"references": [
|
"references": [
|
||||||
{ "path": "./tsconfig.app.json" },
|
{ "path": "./tsconfig.app.json" },
|
||||||
{ "path": "./tsconfig.node.json" }
|
{ "path": "./tsconfig.node.json" }
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -1,24 +1,24 @@
|
|||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
|
||||||
"target": "ES2022",
|
"target": "ES2022",
|
||||||
"lib": ["ES2023"],
|
"lib": ["ES2023"],
|
||||||
"module": "ESNext",
|
"module": "ESNext",
|
||||||
"skipLibCheck": true,
|
"skipLibCheck": true,
|
||||||
|
|
||||||
/* Bundler mode */
|
/* Bundler mode */
|
||||||
"moduleResolution": "bundler",
|
"moduleResolution": "bundler",
|
||||||
"allowImportingTsExtensions": true,
|
"allowImportingTsExtensions": true,
|
||||||
"isolatedModules": true,
|
"isolatedModules": true,
|
||||||
"moduleDetection": "force",
|
"moduleDetection": "force",
|
||||||
"noEmit": true,
|
"noEmit": true,
|
||||||
|
|
||||||
/* Linting */
|
/* Linting */
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"noUnusedLocals": true,
|
"noUnusedLocals": true,
|
||||||
"noUnusedParameters": true,
|
"noUnusedParameters": true,
|
||||||
"noFallthroughCasesInSwitch": true,
|
"noFallthroughCasesInSwitch": true,
|
||||||
"noUncheckedSideEffectImports": true
|
"noUncheckedSideEffectImports": true
|
||||||
},
|
},
|
||||||
"include": ["vite.config.ts"]
|
"include": ["vite.config.ts"]
|
||||||
}
|
}
|
||||||
|
@@ -1,10 +1,10 @@
|
|||||||
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',
|
||||||
},
|
},
|
||||||
});
|
})
|
||||||
|
Reference in New Issue
Block a user