install prettier

This commit is contained in:
2025-09-01 21:07:40 +02:00
parent e4b56e0559
commit 14e822ec5f
31 changed files with 1656 additions and 1190 deletions

View File

@@ -24,31 +24,31 @@ export default tseslint.config({
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,
}, },
}) });
``` ```

View File

@@ -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 },
], ],
}, },
}, },
) );

View File

@@ -26,6 +26,7 @@
"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",
"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",
@@ -980,9 +981,9 @@
} }
}, },
"node_modules/@eslint/config-array": { "node_modules/@eslint/config-array": {
"version": "0.19.2", "version": "0.21.0",
"resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.19.2.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.0.tgz",
"integrity": "sha512-GNKqxfHG2ySmJOBSHg7LxeUx4xpuCoFjacmlCoYWEbaPXLwvfIjixRI12xCQZeULksQb23uiA8F40w5TojpV7w==", "integrity": "sha512-ENIdc4iLu0d93HeYirvKmrzshzofPw6VkZRKQGe9Nv46ZnWUzcF1xV01dcvEg/1wXUR61OmmlSfyeyO7EvjLxQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -995,9 +996,9 @@
} }
}, },
"node_modules/@eslint/config-helpers": { "node_modules/@eslint/config-helpers": {
"version": "0.2.0", "version": "0.3.1",
"resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.2.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.3.1.tgz",
"integrity": "sha512-yJLLmLexii32mGrhW29qvU3QBVTu0GUmEf/J4XsBtVhp4JkIUFN/BjWqTF63yRvGApIDpZm5fa97LtYtINmfeQ==", "integrity": "sha512-xR93k9WhrDYpXHORXpxVL5oHj3Era7wo6k/Wd8/IsQNnZUTzkGS29lyn3nAT05v6ltUuTFVCCYDEGfy2Or/sPA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -1005,9 +1006,9 @@
} }
}, },
"node_modules/@eslint/core": { "node_modules/@eslint/core": {
"version": "0.12.0", "version": "0.15.2",
"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.12.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.15.2.tgz",
"integrity": "sha512-cmrR6pytBuSMTaBweKoGMwu3EiHiEC+DoyupPmlZ0HxBJBtIxwe+j/E4XPIKNx+Q74c8lXKPwYawBf5glsTkHg==", "integrity": "sha512-78Md3/Rrxh83gCxoUc0EiciuOHsIITzLy53m3d9UyiW8y9Dj2D29FeETqyKA+BRK76tnTp6RXWb3pCay8Oyomg==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
@@ -1055,13 +1056,16 @@
} }
}, },
"node_modules/@eslint/js": { "node_modules/@eslint/js": {
"version": "9.23.0", "version": "9.34.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.23.0.tgz", "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.34.0.tgz",
"integrity": "sha512-35MJ8vCPU0ZMxo7zfev2pypqTwWTofFZO6m4KAtdoFhRpLJUpHTZZ+KB3C7Hb1d7bULYwO4lJXGCi5Se+8OMbw==", "integrity": "sha512-EoyvqQnBNsV1CWaEJ559rxXL4c8V92gxirbawSmVUOWXlsRxxQXl6LmCpdUblgxgSkDIqKnhzba2SjRTI/A5Rw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
},
"funding": {
"url": "https://eslint.org/donate"
} }
}, },
"node_modules/@eslint/object-schema": { "node_modules/@eslint/object-schema": {
@@ -1075,13 +1079,13 @@
} }
}, },
"node_modules/@eslint/plugin-kit": { "node_modules/@eslint/plugin-kit": {
"version": "0.2.7", "version": "0.3.5",
"resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.2.7.tgz", "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.3.5.tgz",
"integrity": "sha512-JubJ5B2pJ4k4yGxaNLdbjrnk9d/iDz6/q8wOilpIowd6PJPgaxCuHBnBszq7Ce2TyMrywm5r4PnKm6V3iiZF+g==", "integrity": "sha512-Z5kJ+wU3oA7MMIqVR9tyZRtjYPr4OC004Q4Rw7pgOKUOKkJfZ3O24nz3WYfGRpMDNmcOi3TwQOmgm7B7Tpii0w==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"dependencies": { "dependencies": {
"@eslint/core": "^0.12.0", "@eslint/core": "^0.15.2",
"levn": "^0.4.1" "levn": "^0.4.1"
}, },
"engines": { "engines": {
@@ -2494,9 +2498,9 @@
} }
}, },
"node_modules/acorn": { "node_modules/acorn": {
"version": "8.14.1", "version": "8.15.0",
"resolved": "https://registry.npmjs.org/acorn/-/acorn-8.14.1.tgz", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
"integrity": "sha512-OvQ/2pUDKmgfCg++xsTX1wGxfTaszcHVcTctW4UJB4hibJx2HXxxO5UmVgyjMa+ZDsiaf5wWLXYpRWMmBI0QHg==", "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"bin": { "bin": {
@@ -3193,20 +3197,20 @@
} }
}, },
"node_modules/eslint": { "node_modules/eslint": {
"version": "9.23.0", "version": "9.34.0",
"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.23.0.tgz", "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.34.0.tgz",
"integrity": "sha512-jV7AbNoFPAY1EkFYpLq5bslU9NLNO8xnEeQXwErNibVryjk67wHVmddTBilc5srIttJDBrB0eMHKZBFbSIABCw==", "integrity": "sha512-RNCHRX5EwdrESy3Jc9o8ie8Bog+PeYvvSR8sDGoZxNFTvZ4dlxUB3WzQ3bQMztFrSRODGrLLj8g6OFuGY/aiQg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@eslint-community/eslint-utils": "^4.2.0", "@eslint-community/eslint-utils": "^4.2.0",
"@eslint-community/regexpp": "^4.12.1", "@eslint-community/regexpp": "^4.12.1",
"@eslint/config-array": "^0.19.2", "@eslint/config-array": "^0.21.0",
"@eslint/config-helpers": "^0.2.0", "@eslint/config-helpers": "^0.3.1",
"@eslint/core": "^0.12.0", "@eslint/core": "^0.15.2",
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@eslint/js": "9.23.0", "@eslint/js": "9.34.0",
"@eslint/plugin-kit": "^0.2.7", "@eslint/plugin-kit": "^0.3.5",
"@humanfs/node": "^0.16.6", "@humanfs/node": "^0.16.6",
"@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/module-importer": "^1.0.1",
"@humanwhocodes/retry": "^0.4.2", "@humanwhocodes/retry": "^0.4.2",
@@ -3217,9 +3221,9 @@
"cross-spawn": "^7.0.6", "cross-spawn": "^7.0.6",
"debug": "^4.3.2", "debug": "^4.3.2",
"escape-string-regexp": "^4.0.0", "escape-string-regexp": "^4.0.0",
"eslint-scope": "^8.3.0", "eslint-scope": "^8.4.0",
"eslint-visitor-keys": "^4.2.0", "eslint-visitor-keys": "^4.2.1",
"espree": "^10.3.0", "espree": "^10.4.0",
"esquery": "^1.5.0", "esquery": "^1.5.0",
"esutils": "^2.0.2", "esutils": "^2.0.2",
"fast-deep-equal": "^3.1.3", "fast-deep-equal": "^3.1.3",
@@ -3277,9 +3281,9 @@
} }
}, },
"node_modules/eslint-scope": { "node_modules/eslint-scope": {
"version": "8.3.0", "version": "8.4.0",
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.3.0.tgz", "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz",
"integrity": "sha512-pUNxi75F8MJ/GdeKtVLSbYg4ZI34J6C0C7sbL4YOp2exGwen7ZsuBqKzUhXd0qMQ362yET3z+uPwKeg/0C2XCQ==", "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
@@ -3294,9 +3298,9 @@
} }
}, },
"node_modules/eslint-visitor-keys": { "node_modules/eslint-visitor-keys": {
"version": "4.2.0", "version": "4.2.1",
"resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.0.tgz", "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz",
"integrity": "sha512-UyLnSehNt62FFhSwjZlHmeokpRK59rcz29j+F1/aDgbkbRTk7wIc9XzdoasMUbRNKDM0qQt/+BJ4BrpFeABemw==", "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -3307,15 +3311,15 @@
} }
}, },
"node_modules/espree": { "node_modules/espree": {
"version": "10.3.0", "version": "10.4.0",
"resolved": "https://registry.npmjs.org/espree/-/espree-10.3.0.tgz", "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz",
"integrity": "sha512-0QYC8b24HWY8zjRnDTL6RiHfDbAWn63qb4LMj1Z4b076A4une81+z03Kg7l7mn/48PUTqoLptSXez8oknU8Clg==", "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==",
"dev": true, "dev": true,
"license": "BSD-2-Clause", "license": "BSD-2-Clause",
"dependencies": { "dependencies": {
"acorn": "^8.14.0", "acorn": "^8.15.0",
"acorn-jsx": "^5.3.2", "acorn-jsx": "^5.3.2",
"eslint-visitor-keys": "^4.2.0" "eslint-visitor-keys": "^4.2.1"
}, },
"engines": { "engines": {
"node": "^18.18.0 || ^20.9.0 || >=21.1.0" "node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5739,6 +5743,22 @@
"node": ">= 0.8.0" "node": ">= 0.8.0"
} }
}, },
"node_modules/prettier": {
"version": "3.6.2",
"resolved": "https://registry.npmjs.org/prettier/-/prettier-3.6.2.tgz",
"integrity": "sha512-I7AIg5boAr5R0FFtJ6rCfD+LFsWHp81dolrFD8S79U9tb8Az2nGrJncnMSnys+bpQJfRUzqs9hnA81OAA3hCuQ==",
"dev": true,
"license": "MIT",
"bin": {
"prettier": "bin/prettier.cjs"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/prettier/prettier?sponsor=1"
}
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@@ -7140,21 +7160,6 @@
"dev": true, "dev": true,
"license": "ISC" "license": "ISC"
}, },
"node_modules/yaml": {
"version": "2.7.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.1.tgz",
"integrity": "sha512-10ULxpnOCQXxJvBgxsn9ptjq6uviG/htZKk9veJGhlqn3w/DxQ631zFF+nlQXLwmImeS5amR2dl2U8sg6U9jsQ==",
"dev": true,
"license": "ISC",
"optional": true,
"peer": true,
"bin": {
"yaml": "bin.mjs"
},
"engines": {
"node": ">= 14"
}
},
"node_modules/yargs": { "node_modules/yargs": {
"version": "17.7.2", "version": "17.7.2",
"resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz", "resolved": "https://registry.npmjs.org/yargs/-/yargs-17.7.2.tgz",

View File

@@ -6,8 +6,10 @@
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
"build": "tsc -b && vite build", "build": "tsc -b && vite build",
"lint": "eslint .", "lint": "eslint . --fix",
"preview": "vite preview" "swagger-api": "swagger-typescript-api generate -p http://127.0.0.1:6531/swagger/v2/swagger.json -o ./src/apiClient --modular",
"prettier:check": "prettier . --check",
"prettier": "prettier . --write"
}, },
"dependencies": { "dependencies": {
"@emotion/react": "^11.14.0", "@emotion/react": "^11.14.0",
@@ -28,6 +30,7 @@
"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",
"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",

View File

@@ -10,4 +10,4 @@
position: absolute; position: absolute;
width: 100%; width: 100%;
left: 0; left: 0;
} }

View File

@@ -1,85 +1,89 @@
import Sheet from '@mui/joy/Sheet'; import Sheet from "@mui/joy/Sheet";
import './App.css' import "./App.css";
import Settings from "./Components/Settings/Settings.tsx"; import Settings from "./Components/Settings/Settings.tsx";
import Header from "./Header.tsx"; import Header from "./Header.tsx";
import {createContext, ReactNode, useEffect, useState} from "react"; import { createContext, ReactNode, useEffect, useState } from "react";
import {V2} from "./apiClient/V2.ts"; import { V2 } from "./apiClient/V2.ts";
import { ApiContext } from './apiClient/ApiContext.tsx'; import { ApiContext } from "./apiClient/ApiContext.tsx";
import MangaList from "./Components/Mangas/MangaList.tsx"; import MangaList from "./Components/Mangas/MangaList.tsx";
import {FileLibrary, Manga, MangaConnector} from "./apiClient/data-contracts.ts"; import {
FileLibrary,
Manga,
MangaConnector,
} from "./apiClient/data-contracts.ts";
import Search from "./Components/Search.tsx"; import Search from "./Components/Search.tsx";
import {Typography} from "@mui/joy"; import { Typography } from "@mui/joy";
export const MangaConnectorContext = createContext<MangaConnector[]>([]); export const MangaConnectorContext = createContext<MangaConnector[]>([]);
export const MangaContext = createContext<Manga[]>([]); export const MangaContext = createContext<Manga[]>([]);
export const FileLibraryContext = createContext<FileLibrary[]>([]); export const FileLibraryContext = createContext<FileLibrary[]>([]);
export default function App () { export default function App() {
const apiUriStr = localStorage.getItem("apiUri") ?? window.location.href.substring(0, window.location.href.lastIndexOf("/")) + "/api"; const apiUriStr =
const [apiUri, setApiUri] = useState<string>(apiUriStr); localStorage.getItem("apiUri") ??
const [Api, setApi] = useState<V2>(new V2({ window.location.href.substring(0, window.location.href.lastIndexOf("/")) +
baseUrl: apiUri "/api";
})); const [apiUri, setApiUri] = useState<string>(apiUriStr);
const [Api, setApi] = useState<V2>(
new V2({
baseUrl: apiUri,
}),
);
const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]); const [mangaConnectors, setMangaConnectors] = useState<MangaConnector[]>([]);
const [manga, setManga] = useState<Manga[]>([]); const [manga, setManga] = useState<Manga[]>([]);
const [fileLibraries, setFileLibraries] = useState<FileLibrary[]>([]); const [fileLibraries, setFileLibraries] = useState<FileLibrary[]>([]);
useEffect(() => {
Api.mangaConnectorList().then(response => {
if (response.ok)
setMangaConnectors(response.data);
});
Api.fileLibraryList().then(response => {
if (response.ok)
setFileLibraries(response.data);
})
Api.queryMangaDownloadingList()
.then(response => {
if (response.ok)
setManga(response.data);
});
}, []);
useEffect(() => {
localStorage.setItem("apiUri", apiUri);
if (Api.baseUrl != apiUri)
setApi(new V2({
baseUrl: apiUri
}));
}, [apiUri]);
return ( useEffect(() => {
<ApiContext.Provider value={Api}> Api.mangaConnectorList().then((response) => {
<FileLibraryContext value={fileLibraries}> if (response.ok) setMangaConnectors(response.data);
<MangaConnectorContext.Provider value={mangaConnectors}> });
<MangaContext.Provider value={manga}>
{ Api.fileLibraryList().then((response) => {
Api ? if (response.ok) setFileLibraries(response.data);
<Sheet className={"app"}> });
<Header>
<Settings setApiUri={setApiUri} /> Api.queryMangaDownloadingList().then((response) => {
</Header> if (response.ok) setManga(response.data);
<Sheet className={"app-content"}> });
<MangaList mangas={manga}> }, []);
<Search />
</MangaList> useEffect(() => {
</Sheet> localStorage.setItem("apiUri", apiUri);
</Sheet> if (Api.baseUrl != apiUri)
: <Loading /> setApi(
} new V2({
</MangaContext.Provider> baseUrl: apiUri,
</MangaConnectorContext.Provider> }),
</FileLibraryContext> );
</ApiContext.Provider> }, [apiUri]);
);
return (
<ApiContext.Provider value={Api}>
<FileLibraryContext value={fileLibraries}>
<MangaConnectorContext.Provider value={mangaConnectors}>
<MangaContext.Provider value={manga}>
{Api ? (
<Sheet className={"app"}>
<Header>
<Settings setApiUri={setApiUri} />
</Header>
<Sheet className={"app-content"}>
<MangaList mangas={manga}>
<Search />
</MangaList>
</Sheet>
</Sheet>
) : (
<Loading />
)}
</MangaContext.Provider>
</MangaConnectorContext.Provider>
</FileLibraryContext>
</ApiContext.Provider>
);
} }
function Loading () : ReactNode{ function Loading(): ReactNode {
return ( return <Typography>Loading</Typography>;
<Typography>Loading</Typography> }
);
}

View File

@@ -1,32 +1,34 @@
import {Close, Done} from "@mui/icons-material"; import { Close, Done } from "@mui/icons-material";
import {CircularProgress, ColorPaletteProp} from "@mui/joy"; import { CircularProgress, ColorPaletteProp } from "@mui/joy";
import {ReactNode} from "react"; import { ReactNode } from "react";
export enum LoadingState { export enum LoadingState {
none, none,
loading, loading,
success, success,
failure failure,
} }
export function StateIndicator(state : LoadingState) : ReactNode { export function StateIndicator(state: LoadingState): ReactNode {
switch (state) { switch (state) {
case LoadingState.loading: case LoadingState.loading:
return (<CircularProgress />); return <CircularProgress />;
case LoadingState.failure: case LoadingState.failure:
return (<Close />); return <Close />;
case LoadingState.success: case LoadingState.success:
return (<Done />); return <Done />;
default: return null; default:
} return null;
}
} }
export function StateColor(state : LoadingState) : ColorPaletteProp | undefined { export function StateColor(state: LoadingState): ColorPaletteProp | undefined {
switch (state) { switch (state) {
case LoadingState.failure: case LoadingState.failure:
return "danger"; return "danger";
case LoadingState.success: case LoadingState.success:
return "success"; return "success";
default: return undefined; default:
} return undefined;
} }
}

View File

@@ -1,42 +1,97 @@
import {CSSProperties, ReactNode, useContext, useEffect, useRef, useState} from "react"; import {
import {ChapterMangaConnectorId, MangaConnector, MangaMangaConnectorId} from "../apiClient/data-contracts.ts"; CSSProperties,
import {Link, Tooltip, Typography} from "@mui/joy"; ReactNode,
import {MangaConnectorContext} from "../App.tsx"; useContext,
import {ApiContext} from "../apiClient/ApiContext.tsx"; 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{ export default function MangaConnectorLink({
const mangaConnectorContext = useContext(MangaConnectorContext); MangaConnectorId,
const [mangaConnector, setMangaConnector] = useState<MangaConnector | undefined>(mangaConnectorContext?.find(c => c.name == MangaConnectorId.mangaConnectorName)); imageStyle,
const imageRef = useRef<HTMLImageElement | null>(null); printName,
}: {
useEffect(() => { MangaConnectorId: MangaMangaConnectorId | ChapterMangaConnectorId;
const connector = mangaConnectorContext?.find(c => c.name == MangaConnectorId.mangaConnectorName); imageStyle?: CSSProperties;
setMangaConnector(connector); printName?: boolean;
if (imageRef?.current != null) }): ReactNode {
imageRef.current.setHTMLUnsafe(`<img ref=${imageRef} src=${mangaConnector?.iconUrl} style=${imageStyle}/>`); const mangaConnectorContext = useContext(MangaConnectorContext);
}, []); const [mangaConnector, setMangaConnector] = useState<
MangaConnector | undefined
return ( >(
<Tooltip title={<Typography>{MangaConnectorId.mangaConnectorName}: <Link href={MangaConnectorId.websiteUrl as string}>{MangaConnectorId.websiteUrl}</Link></Typography>}> mangaConnectorContext?.find(
<Link href={MangaConnectorId.websiteUrl as string}> (c) => c.name == MangaConnectorId.mangaConnectorName,
<img ref={imageRef} src={mangaConnector?.iconUrl} style={imageStyle}/> ),
{printName ? <Typography>{mangaConnector?.name}</Typography> : null} );
</Link> const imageRef = useRef<HTMLImageElement | null>(null);
</Tooltip>
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}/>`,
);
}, []);
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} />
{printName ? <Typography>{mangaConnector?.name}</Typography> : null}
</Link>
</Tooltip>
);
} }
export function MangaConnectorLinkFromId({MangaConnectorIdId, imageStyle, printName} : {MangaConnectorIdId: string, imageStyle? : CSSProperties, printName?: boolean}) : ReactNode { export function MangaConnectorLinkFromId({
const Api = useContext(ApiContext); MangaConnectorIdId,
imageStyle,
const [node, setNode] = useState<ReactNode>(null); printName,
}: {
MangaConnectorIdId: string;
imageStyle?: CSSProperties;
printName?: boolean;
}): ReactNode {
const Api = useContext(ApiContext);
useEffect(() => { const [node, setNode] = useState<ReactNode>(null);
Api.queryMangaMangaConnectorIdDetail(MangaConnectorIdId).then(response => {
if (response.ok) useEffect(() => {
setNode(<MangaConnectorLink key={response.data.key} MangaConnectorId={response.data} imageStyle={{...imageStyle, width: "25px"}} printName={printName} />); Api.queryMangaMangaConnectorIdDetail(MangaConnectorIdId).then(
}); (response) => {
}, []); if (response.ok)
setNode(
return node; <MangaConnectorLink
} key={response.data.key}
MangaConnectorId={response.data}
imageStyle={{ ...imageStyle, width: "25px" }}
printName={printName}
/>,
);
},
);
}, []);
return node;
}

View File

@@ -1,21 +1,25 @@
.manga-card { .manga-card {
width: 220px; width: 220px;
height: 300px; height: 300px;
} }
.manga-cover-blur { .manga-cover-blur {
background: linear-gradient(135deg, rgba(245, 169, 184, 0.85) 40%, rgba(91, 206, 250, 0.3)); background: linear-gradient(
box-shadow: 0 4px 30px rgba(0, 0, 0, 0.1);) 135deg,
backdrop-filter: blur(6px); rgba(245, 169, 184, 0.85) 40%,
-webkit-backdrop-filter: blur(6px); 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-badge-icon {
width: 25px; width: 25px;
height: 25px; height: 25px;
} }
.manga-modal { .manga-modal {
width: 90%; width: 90%;
margin: auto; margin: auto;
} }

View File

@@ -1,105 +1,176 @@
import { import {
Badge, Badge,
Box, Box,
Card, Card,
CardContent, CardContent,
CardCover, CardCover,
Chip, Chip,
Link, Link,
Modal, Modal,
ModalDialog, ModalDialog,
Stack, Tooltip, Stack,
Typography Tooltip,
Typography,
} from "@mui/joy"; } from "@mui/joy";
import {Manga} from "../../apiClient/data-contracts.ts"; import { Manga } from "../../apiClient/data-contracts.ts";
import {Dispatch, ReactNode, SetStateAction, useContext, useState} from "react"; import {
Dispatch,
ReactNode,
SetStateAction,
useContext,
useState,
} from "react";
import "./MangaCard.css"; import "./MangaCard.css";
import MangaConnectorBadge from "./MangaConnectorBadge.tsx"; import MangaConnectorBadge from "./MangaConnectorBadge.tsx";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
import MarkdownPreview from '@uiw/react-markdown-preview'; import MarkdownPreview from "@uiw/react-markdown-preview";
import {MangaContext} from "../../App.tsx"; import { MangaContext } from "../../App.tsx";
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx"; import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx";
export function MangaCardFromId({mangaId} : {mangaId: string}) { export function MangaCardFromId({ mangaId }: { mangaId: string }) {
const mangas = useContext(MangaContext); const mangas = useContext(MangaContext);
const manga = mangas.find(manga => manga.key === mangaId); const manga = mangas.find((manga) => manga.key === mangaId);
return <MangaCard manga={manga} /> return <MangaCard manga={manga} />;
} }
export function MangaCard({manga, children} : {manga: Manga | undefined, children? : ReactNode}) { export function MangaCard({
if (manga === undefined) manga,
return PlaceHolderCard(); children,
}: {
manga: Manga | undefined;
children?: ReactNode;
}) {
if (manga === undefined) return PlaceHolderCard();
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<MangaConnectorBadge manga={manga}> <MangaConnectorBadge manga={manga}>
<Card className={"manga-card"} onClick={() => setOpen(true)}> <Card className={"manga-card"} onClick={() => setOpen(true)}>
<CardCover className={"manga-cover"}> <CardCover className={"manga-cover"}>
<MangaCover manga={manga} /> <MangaCover manga={manga} />
</CardCover> </CardCover>
<CardCover className={"manga-cover-blur"} /> <CardCover className={"manga-cover-blur"} />
<CardContent className={"manga-content"}> <CardContent className={"manga-content"}>
<Typography level={"title-lg"}>{manga?.name}</Typography> <Typography level={"title-lg"}>{manga?.name}</Typography>
</CardContent> </CardContent>
</Card> </Card>
<MangaModal manga={manga} open={open} setOpen={setOpen}> <MangaModal manga={manga} open={open} setOpen={setOpen}>
{children} {children}
</MangaModal> </MangaModal>
</MangaConnectorBadge> </MangaConnectorBadge>
); );
} }
export function MangaModal({manga, open, setOpen, children}: {manga: Manga | undefined, open: boolean, setOpen: Dispatch<SetStateAction<boolean>>, children?: ReactNode}) { export function MangaModal({
manga,
return ( open,
<Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}> setOpen,
<ModalDialog style={{width:'100%'}}> children,
<ModalClose /> }: {
<Tooltip title={<Stack spacing={1}>{manga?.altTitles?.map(title => <Chip>{title.title}</Chip>)}</Stack>}> manga: Manga | undefined;
<Typography level={"h4"} width={"fit-content"}>{manga?.name}</Typography> open: boolean;
</Tooltip> setOpen: Dispatch<SetStateAction<boolean>>;
<Stack direction={"row"} spacing={2}> children?: ReactNode;
<Box key={"Cover"} className={"manga-card"}> }) {
<MangaCover manga={manga} /> return (
</Box> <Modal open={open} onClose={() => setOpen(false)} className={"manga-modal"}>
<Stack key={"Description"} direction={"column"} sx={{width: "calc(100% - 230px)"}}> <ModalDialog style={{ width: "100%" }}>
<Stack key={"Tags"} direction={"row"} flexWrap={"wrap"} useFlexGap spacing={0.5}> <ModalClose />
{manga?.mangaConnectorIdsIds?.map((idid) => <MangaConnectorLinkFromId key={idid} MangaConnectorIdId={idid} />)} <Tooltip
{manga?.mangaTags?.map((tag) => <Chip key={tag.tag}>{tag.tag}</Chip>)} title={
{manga?.links?.map((link) => <Chip key={link.key}><Link href={link.linkUrl}>{link.linkProvider}</Link></Chip>)} <Stack spacing={1}>
</Stack> {manga?.altTitles?.map((title) => (
<Box sx={{flexGrow: 1}}> <Chip>{title.title}</Chip>
<MarkdownPreview source={manga?.description} style={{background: "transparent"}}/> ))}
</Box> </Stack>
<Stack sx={{justifySelf: "flex-end", alignSelf: "flex-end"}} spacing={2} direction={"row"}>{children}</Stack> }
</Stack> >
</Stack> <Typography level={"h4"} width={"fit-content"}>
</ModalDialog> {manga?.name}
</Modal> </Typography>
); </Tooltip>
<Stack direction={"row"} spacing={2}>
<Box key={"Cover"} className={"manga-card"}>
<MangaCover manga={manga} />
</Box>
<Stack
key={"Description"}
direction={"column"}
sx={{ width: "calc(100% - 230px)" }}
>
<Stack
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}
style={{ background: "transparent" }}
/>
</Box>
<Stack
sx={{ justifySelf: "flex-end", alignSelf: "flex-end" }}
spacing={2}
direction={"row"}
>
{children}
</Stack>
</Stack>
</Stack>
</ModalDialog>
</Modal>
);
} }
function PlaceHolderCard(){ function PlaceHolderCard() {
return ( return (
<Badge> <Badge>
<Card className={"manga-card"}> <Card className={"manga-card"}>
<CardCover className={"manga-cover"}> <CardCover className={"manga-cover"}>
<img src={"/blahaj.png"} /> <img src={"/blahaj.png"} />
</CardCover> </CardCover>
<CardCover className={"manga-cover-blur"} /> <CardCover className={"manga-cover-blur"} />
</Card> </Card>
</Badge> </Badge>
); );
} }
function MangaCover({manga}: {manga: Manga | undefined}) { function MangaCover({ manga }: { manga: Manga | undefined }) {
const api = useContext(ApiContext); const api = useContext(ApiContext);
const uri = manga ? `${api.baseUrl}/v2/Manga/${manga?.key}/Cover` : "blahaj.png"; const uri = manga
? `${api.baseUrl}/v2/Manga/${manga?.key}/Cover`
return ( : "blahaj.png";
<img src={uri} style={{width: "100%", height: "100%", objectFit: "cover", borderRadius: "var(--CardCover-radius)"}} />
); return (
} <img
src={uri}
style={{
width: "100%",
height: "100%",
objectFit: "cover",
borderRadius: "var(--CardCover-radius)",
}}
/>
);
}

View File

@@ -1,14 +1,23 @@
import { Badge } from "@mui/joy"; import { Badge } from "@mui/joy";
import {Manga} from "../../apiClient/data-contracts.ts"; import { Manga } from "../../apiClient/data-contracts.ts";
import {ReactElement} from "react"; import { ReactElement } from "react";
import "./MangaCard.css" import "./MangaCard.css";
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx"; import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx";
export default function MangaConnectorBadge ({manga, children} : {manga: Manga, children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) { export default function MangaConnectorBadge({
manga,
return ( children,
<Badge badgeContent={manga.mangaConnectorIdsIds?.map(id => <MangaConnectorLinkFromId key={id} MangaConnectorIdId={id} />)}> }: {
{children} manga: Manga;
</Badge> children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined;
); }) {
} return (
<Badge
badgeContent={manga.mangaConnectorIdsIds?.map((id) => (
<MangaConnectorLinkFromId key={id} MangaConnectorIdId={id} />
))}
>
{children}
</Badge>
);
}

View File

@@ -1,100 +1,138 @@
import {Manga} from "../../apiClient/data-contracts.ts"; import { Manga } from "../../apiClient/data-contracts.ts";
import {ChangeEvent, Dispatch, ReactNode, useContext, useEffect, useState} from "react"; import {
import {Button, Checkbox, Option, Select, Stack, Typography} from "@mui/joy"; 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 Drawer from "@mui/joy/Drawer";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {MangaConnectorLinkFromId} from "../MangaConnectorLink.tsx"; import { MangaConnectorLinkFromId } from "../MangaConnectorLink.tsx";
import Sheet from "@mui/joy/Sheet"; import Sheet from "@mui/joy/Sheet";
import {FileLibraryContext} from "../../App.tsx"; import { FileLibraryContext } from "../../App.tsx";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
import {LoadingState, StateIndicator} from "../Loading.tsx"; import { LoadingState, StateIndicator } from "../Loading.tsx";
export default function ({manga} : {manga: Manga}) : ReactNode{ export default function ({ manga }: { manga: Manga }): ReactNode {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<> <>
<Button onClick={() => setOpen(true)}>Download</Button> <Button onClick={() => setOpen(true)}>Download</Button>
<DownloadDrawer manga={manga} open={open} setOpen={setOpen} /> <DownloadDrawer manga={manga} open={open} setOpen={setOpen} />
</> </>
); );
} }
function DownloadDrawer({manga, open, setOpen}: {manga: Manga, open: boolean, setOpen: Dispatch<boolean>}): ReactNode { function DownloadDrawer({
const fileLibraries = useContext(FileLibraryContext); manga,
const Api = useContext(ApiContext); open,
setOpen,
const onLibraryChange = (_ :any, value: ({} | null)) => { }: {
if (value === undefined) manga: Manga;
return; open: boolean;
Api.mangaChangeLibraryCreate(manga.key as string, value as string); setOpen: Dispatch<boolean>;
} }): ReactNode {
const fileLibraries = useContext(FileLibraryContext);
return ( const Api = useContext(ApiContext);
<Drawer open={open} onClose={() => setOpen(false)}>
<ModalClose /> const onLibraryChange = (_: any, value: {} | null) => {
<Sheet sx={{width: "calc(95% - 60px)", margin: "30px"}}> if (value === undefined) return;
<Typography>Download to Library:</Typography> Api.mangaChangeLibraryCreate(manga.key as string, value as string);
<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> return (
))} <Drawer open={open} onClose={() => setOpen(false)}>
</Select> <ModalClose />
<Typography>Download from:</Typography> <Sheet sx={{ width: "calc(95% - 60px)", margin: "30px" }}>
<Stack> <Typography>Download to Library:</Typography>
{manga.mangaConnectorIdsIds?.map(id => <DownloadCheckBox key={id} mangaConnectorIdId={id} />)} <Select
</Stack> placeholder={"Library"}
</Sheet> onChange={onLibraryChange}
</Drawer> 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 { function DownloadCheckBox({
const Api = useContext(ApiContext); mangaConnectorIdId,
const [useForDownloading, setUseForDownloading] = useState<boolean>(false); }: {
const [loading, setLoading] = useState<LoadingState>(LoadingState.none); mangaConnectorIdId: string;
}): ReactNode {
useEffect(() => { const Api = useContext(ApiContext);
setLoading(LoadingState.loading); const [useForDownloading, setUseForDownloading] = useState<boolean>(false);
Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId).then(response => { const [loading, setLoading] = useState<LoadingState>(LoadingState.none);
if (response.ok){
setUseForDownloading(response.data.useForDownload as boolean); useEffect(() => {
setLoading(LoadingState.none); setLoading(LoadingState.loading);
}else Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId)
setLoading(LoadingState.failure); .then((response) => {
}).catch(_ => setLoading(LoadingState.failure)); if (response.ok) {
}, []); setUseForDownloading(response.data.useForDownload as boolean);
setLoading(LoadingState.none);
const onSelected = (event: ChangeEvent<HTMLInputElement>) => { } else setLoading(LoadingState.failure);
setLoading(LoadingState.loading); })
const val = event.currentTarget.checked; .catch((_) => setLoading(LoadingState.failure));
Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId).then(response => { }, []);
if (!response.ok){
setLoading(LoadingState.failure); const onSelected = (event: ChangeEvent<HTMLInputElement>) => {
return; setLoading(LoadingState.loading);
} const val = event.currentTarget.checked;
Api.mangaSetAsDownloadFromCreate(response.data.objId, response.data.mangaConnectorName, val) Api.queryMangaMangaConnectorIdDetail(mangaConnectorIdId)
.then(response => { .then((response) => {
if (response.ok){ if (!response.ok) {
setUseForDownloading(val); setLoading(LoadingState.failure);
setLoading(LoadingState.success); return;
}else }
setLoading(LoadingState.failure); Api.mangaSetAsDownloadFromCreate(
}).catch(_ => setLoading(LoadingState.failure)); response.data.objId,
}).catch(_ => setLoading(LoadingState.failure)); response.data.mangaConnectorName,
} val,
)
return ( .then((response) => {
<Checkbox if (response.ok) {
indeterminateIcon={StateIndicator(LoadingState.loading)} setUseForDownloading(val);
indeterminate={loading === LoadingState.loading} setLoading(LoadingState.success);
disabled={loading === LoadingState.loading} } else setLoading(LoadingState.failure);
checked={useForDownloading} })
onChange={onSelected} .catch((_) => setLoading(LoadingState.failure));
label={ })
<Typography> .catch((_) => setLoading(LoadingState.failure));
<MangaConnectorLinkFromId MangaConnectorIdId={mangaConnectorIdId} printName={true} /> };
</Typography>
} /> 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>
}
/>
);
}

View File

@@ -1,3 +1,3 @@
.manga-list { .manga-list {
padding-top: 12px; padding-top: 12px;
} }

View File

@@ -1,28 +1,38 @@
import {ReactNode} from "react"; import { ReactNode } from "react";
import {MangaCard} from "./MangaCard.tsx"; import { MangaCard } from "./MangaCard.tsx";
import {Stack} from "@mui/joy"; import { Stack } from "@mui/joy";
import "./MangaList.css"; import "./MangaList.css";
import {Manga} from "../../apiClient/data-contracts.ts"; import { Manga } from "../../apiClient/data-contracts.ts";
import MangaDownloadDialog from "./MangaDownloadDialog.tsx"; import MangaDownloadDialog from "./MangaDownloadDialog.tsx";
import MangaMerge from "./MangaMerge.tsx"; import MangaMerge from "./MangaMerge.tsx";
export default function MangaList ({mangas, children} : {mangas: Manga[], children?: ReactNode}) { export default function MangaList({
mangas,
return ( children,
<Stack className={"manga-list"} direction={"row"} useFlexGap={true} spacing={2} flexWrap={"wrap"} sx={ }: {
{ mangas: Manga[];
mx: 'calc(-1 * var(--ModalDialog-padding))', children?: ReactNode;
px: 'var(--ModalDialog-padding)', }) {
overflowY: 'scroll' return (
}}> <Stack
{children} className={"manga-list"}
{mangas?.map(manga => ( direction={"row"}
<MangaCard key={manga.key} manga={manga}> useFlexGap={true}
<MangaDownloadDialog manga={manga} /> spacing={2}
<MangaMerge manga={manga} /> flexWrap={"wrap"}
</MangaCard> sx={{
))} mx: "calc(-1 * var(--ModalDialog-padding))",
</Stack> px: "var(--ModalDialog-padding)",
); overflowY: "scroll",
}}
} >
{children}
{mangas?.map((manga) => (
<MangaCard key={manga.key} manga={manga}>
<MangaDownloadDialog manga={manga} />
<MangaMerge manga={manga} />
</MangaCard>
))}
</Stack>
);
}

View File

@@ -1,105 +1,136 @@
import {ReactNode, useContext, useEffect, useState} from "react"; import { ReactNode, useContext, useEffect, useState } from "react";
import {Manga} from "../../apiClient/data-contracts.ts"; import { Manga } from "../../apiClient/data-contracts.ts";
import Drawer from "@mui/joy/Drawer"; import Drawer from "@mui/joy/Drawer";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
import {MangaCard} from "./MangaCard.tsx"; import { MangaCard } from "./MangaCard.tsx";
import {Alert, Button, Modal, ModalDialog, Stack, Tooltip, Typography} from "@mui/joy"; import {
import {KeyboardDoubleArrowRight, Warning} from "@mui/icons-material"; Alert,
import {LoadingState, StateIndicator} from "../Loading.tsx"; 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 : Manga | undefined}) : ReactNode { export default function ({ manga }: { manga: Manga | undefined }): ReactNode {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [similar, setSimilar] = useState<Manga[]>([]); const [similar, setSimilar] = useState<Manga[]>([]);
const [open, setOpen] = useState<boolean>(false); const [open, setOpen] = useState<boolean>(false);
useEffect(()=> { useEffect(() => {
if (manga === undefined || !open) if (manga === undefined || !open) return;
return; Api.queryMangaSimilarNameList(manga.key as string).then((response) => {
Api.queryMangaSimilarNameList(manga.key as string).then(response => { if (response.ok)
if (response.ok) Api.mangaWithIDsCreate(response.data).then((response) => {
Api.mangaWithIDsCreate(response.data).then(response => { if (response.ok) setSimilar(response.data);
if (response.ok)
setSimilar(response.data);
});
}); });
}, [open]); });
}, [open]);
const exit = (manga : Manga) => {
setOpen(false); const exit = (manga: Manga) => {
setSimilar(similar.filter(m => m.key != manga.key)); setOpen(false);
} setSimilar(similar.filter((m) => m.key != manga.key));
};
return (
<> return (
<Button onClick={_ => setOpen(true)}> <>
Merge <Button onClick={(_) => setOpen(true)}>Merge</Button>
</Button> <Drawer
<Drawer size={"lg"} open={open} onClose={() => setOpen(false)} anchor={"bottom"}> size={"lg"}
<ModalClose /> open={open}
<Stack direction={"row"} spacing={2} flexWrap={"wrap"} useFlexGap> onClose={() => setOpen(false)}
{similar.map(similarManga => <MangaCard manga={similarManga}> anchor={"bottom"}
<ConfirmationModal manga={manga as Manga} similarManga={similarManga} exit={() => exit(similarManga)} /> >
</MangaCard>)} <ModalClose />
</Stack> <Stack direction={"row"} spacing={2} flexWrap={"wrap"} useFlexGap>
</Drawer> {similar.map((similarManga) => (
</> <MangaCard manga={similarManga}>
); <ConfirmationModal
manga={manga as Manga}
similarManga={similarManga}
exit={() => exit(similarManga)}
/>
</MangaCard>
))}
</Stack>
</Drawer>
</>
);
} }
function ConfirmationModal({manga, similarManga, exit} : {manga : Manga, similarManga : Manga, exit: ()=>void}) : ReactNode { function ConfirmationModal({
const [open, setOpen] = useState<boolean>(false); manga,
const Api = useContext(ApiContext); 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 [loadingState, setLoadingState] = useState<LoadingState>(
LoadingState.none,
const merge = () => { );
setLoadingState(LoadingState.loading);
Api.mangaMergeIntoPartialUpdate(manga.key as string, similarManga.key as string).then(response => { const merge = () => {
if (response.ok){ setLoadingState(LoadingState.loading);
setLoadingState(LoadingState.success); Api.mangaMergeIntoPartialUpdate(
setOpen(false); manga.key as string,
exit(); similarManga.key as string,
}else )
setLoadingState(LoadingState.failure); .then((response) => {
}).catch(_ => setLoadingState(LoadingState.failure)); if (response.ok) {
} setLoadingState(LoadingState.success);
setOpen(false);
return <> exit();
{ } else setLoadingState(LoadingState.failure);
loadingState == LoadingState.success ? })
<Alert .catch((_) => setLoadingState(LoadingState.failure));
color="success" };
startDecorator={StateIndicator(LoadingState.success)}
> return (
Merged Successfully! <>
</Alert> {loadingState == LoadingState.success ? (
: <Alert
<Button onClick={_ => setOpen(true)}> color="success"
Merge into startDecorator={StateIndicator(LoadingState.success)}
</Button> >
} Merged Successfully!
<Modal open={open} onClose={_ => setOpen(false)}> </Alert>
<ModalDialog> ) : (
<ModalClose /> <Button onClick={(_) => setOpen(true)}>Merge into</Button>
<Typography level={"h2"}>Confirm Merge</Typography> )}
<Stack direction={"row"} spacing={3} alignItems={"center"}> <Modal open={open} onClose={(_) => setOpen(false)}>
<MangaCard manga={manga} /> <ModalDialog>
<Typography color={"danger"} level={"h1"}><KeyboardDoubleArrowRight /></Typography> <ModalClose />
<MangaCard manga={similarManga} /> <Typography level={"h2"}>Confirm Merge</Typography>
</Stack> <Stack direction={"row"} spacing={3} alignItems={"center"}>
<Tooltip title={"THIS CAN NOT BE UNDONE!"}> <MangaCard manga={manga} />
<Button <Typography color={"danger"} level={"h1"}>
onClick={merge} <KeyboardDoubleArrowRight />
disabled={loadingState === LoadingState.loading} </Typography>
endDecorator={StateIndicator(loadingState)} <MangaCard manga={similarManga} />
color={"danger"} </Stack>
startDecorator={<Warning />}> <Tooltip title={"THIS CAN NOT BE UNDONE!"}>
Merge <Button
</Button> onClick={merge}
</Tooltip> disabled={loadingState === LoadingState.loading}
</ModalDialog> endDecorator={StateIndicator(loadingState)}
</Modal> color={"danger"}
</>; startDecorator={<Warning />}
} >
Merge
</Button>
</Tooltip>
</ModalDialog>
</Modal>
</>
);
}

View File

@@ -1,138 +1,175 @@
import {Dispatch, KeyboardEventHandler, ReactNode, useContext, useState} from "react";
import { import {
Badge, Dispatch,
Button, KeyboardEventHandler,
Card, ReactNode,
CardContent, useContext,
CardCover, Chip, useState,
Input, } from "react";
Modal, import {
ModalDialog, Badge,
Option, Button,
Select, Card,
Step, CardContent,
StepIndicator, CardCover,
Stepper, Chip,
Typography Input,
Modal,
ModalDialog,
Option,
Select,
Step,
StepIndicator,
Stepper,
Typography,
} from "@mui/joy"; } from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {MangaConnectorContext} from "../App.tsx"; import { MangaConnectorContext } from "../App.tsx";
import {Manga, MangaConnector} from "../apiClient/data-contracts.ts"; import { Manga, MangaConnector } from "../apiClient/data-contracts.ts";
import MangaList from "./Mangas/MangaList.tsx"; import MangaList from "./Mangas/MangaList.tsx";
import {ApiContext} from "../apiClient/ApiContext.tsx"; import { ApiContext } from "../apiClient/ApiContext.tsx";
import {LoadingState, StateColor, StateIndicator} from "./Loading.tsx"; import { LoadingState, StateColor, StateIndicator } from "./Loading.tsx";
export default function () : ReactNode { export default function (): ReactNode {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
return ( return (
<Badge badgeContent={"+"}> <Badge badgeContent={"+"}>
<Card onClick={() => {if (!open) setOpen(true)}} className={"manga-card"}> <Card
<CardCover className={"manga-cover"}> onClick={() => {
<img src={"/blahaj.png"} /> if (!open) setOpen(true);
</CardCover> }}
<CardCover className={"manga-cover-blur"} /> className={"manga-card"}
<CardContent> >
Add <CardCover className={"manga-cover"}>
</CardContent> <img src={"/blahaj.png"} />
<CardContent> </CardCover>
<SearchDialog open={open} setOpen={setOpen} /> <CardCover className={"manga-cover-blur"} />
</CardContent> <CardContent>Add</CardContent>
</Card> <CardContent>
</Badge> <SearchDialog open={open} setOpen={setOpen} />
); </CardContent>
</Card>
</Badge>
);
} }
function SearchDialog ({open, setOpen} : {open: boolean, setOpen: Dispatch<boolean>}) : ReactNode { function SearchDialog({
const mangaConnectors = useContext(MangaConnectorContext); open,
const Api = useContext(ApiContext); setOpen,
}: {
const [selectedMangaConnector, setSelectedMangaConnector] = useState<MangaConnector | undefined>(undefined); open: boolean;
const [searchTerm, setSearchTerm] = useState<string>(); setOpen: Dispatch<boolean>;
const [searchResults, setSearchResults] = useState<Manga[]>([]); }): ReactNode {
const mangaConnectors = useContext(MangaConnectorContext);
const Api = useContext(ApiContext);
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none); const [selectedMangaConnector, setSelectedMangaConnector] = useState<
MangaConnector | undefined
const doTheSearch = () => { >(undefined);
if (searchTerm === undefined || searchTerm.length < 1) const [searchTerm, setSearchTerm] = useState<string>();
return; const [searchResults, setSearchResults] = useState<Manga[]>([]);
if (!isUrl(searchTerm) && selectedMangaConnector === undefined)
return; const [loadingState, setLoadingState] = useState<LoadingState>(
LoadingState.none,
setLoadingState(LoadingState.loading); );
if (isUrl(searchTerm)) const doTheSearch = () => {
Api.searchUrlCreate(searchTerm) if (searchTerm === undefined || searchTerm.length < 1) return;
.then(response => { if (!isUrl(searchTerm) && selectedMangaConnector === undefined) return;
if (response.ok){
setSearchResults([response.data]); setLoadingState(LoadingState.loading);
setLoadingState(LoadingState.success);
}else if (isUrl(searchTerm))
setLoadingState(LoadingState.failure); Api.searchUrlCreate(searchTerm)
}).catch(() => setLoadingState(LoadingState.failure)); .then((response) => {
else if (response.ok) {
Api.searchDetail(selectedMangaConnector!.name, searchTerm) setSearchResults([response.data]);
.then(response => { setLoadingState(LoadingState.success);
if(response.ok){ } else setLoadingState(LoadingState.failure);
setSearchResults(response.data); })
setLoadingState(LoadingState.success); .catch(() => setLoadingState(LoadingState.failure));
}else else
setLoadingState(LoadingState.failure); Api.searchDetail(selectedMangaConnector!.name, searchTerm)
}).catch(() => setLoadingState(LoadingState.failure)); .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 (Error) {
return false;
} }
};
const isUrl = (url: string) => {
try{ const keyDownCheck: KeyboardEventHandler<HTMLInputElement> = (e) => {
new URL(url); if (e.key === "Enter") {
return true; doTheSearch();
}catch (Error){
return false;
}
} }
};
const keyDownCheck : KeyboardEventHandler<HTMLInputElement> = (e) => {
if (e.key === "Enter") { return (
doTheSearch(); <Modal
} sx={{ width: "100%", height: "100%" }}
} open={open}
onClose={() => setOpen(false)}
return ( >
<Modal sx={{width: "100%", height: "100%"}} open={open} onClose={() => setOpen(false)}> <ModalDialog sx={{ width: "80%" }}>
<ModalDialog sx={{width: "80%"}}> <ModalClose />
<ModalClose /> <Stepper orientation={"vertical"}>
<Stepper orientation={"vertical"}> <Step indicator={<StepIndicator>1</StepIndicator>}>
<Step indicator={<StepIndicator>1</StepIndicator>}> <Typography>Connector</Typography>
<Typography>Connector</Typography> <Select
<Select disabled={loadingState == LoadingState.loading}
disabled={loadingState == LoadingState.loading} onChange={(_, v) =>
onChange={(_, v) => setSelectedMangaConnector(v as MangaConnector)}> setSelectedMangaConnector(v as MangaConnector)
{mangaConnectors?.map(con => ( }
<Option value={con}> >
<Typography><img src={con.iconUrl} style={{maxHeight: "var(--Icon-fontSize)"}} />{con.name}</Typography> {mangaConnectors?.map((con) => (
</Option> <Option value={con}>
))} <Typography>
</Select> <img
</Step> src={con.iconUrl}
<Step indicator={<StepIndicator>2</StepIndicator>}> style={{ maxHeight: "var(--Icon-fontSize)" }}
<Typography>Search</Typography> />
<Input {con.name}
disabled={loadingState == LoadingState.loading} </Typography>
onKeyDown={keyDownCheck} </Option>
onChange={(e) => setSearchTerm(e.currentTarget.value)} ))}
endDecorator={<Button </Select>
disabled={loadingState == LoadingState.loading} </Step>
onClick={doTheSearch} <Step indicator={<StepIndicator>2</StepIndicator>}>
endDecorator={StateIndicator(loadingState)} <Typography>Search</Typography>
color={StateColor(loadingState)} <Input
>Search</Button>} disabled={loadingState == LoadingState.loading}
/> onKeyDown={keyDownCheck}
</Step> onChange={(e) => setSearchTerm(e.currentTarget.value)}
<Step indicator={<StepIndicator>3</StepIndicator>}> endDecorator={
<Typography>Result <Chip>{searchResults.length}</Chip></Typography> <Button
<MangaList mangas={searchResults} /> disabled={loadingState == LoadingState.loading}
</Step> onClick={doTheSearch}
</Stepper> endDecorator={StateIndicator(loadingState)}
</ModalDialog> color={StateColor(loadingState)}
</Modal> >
); Search
} </Button>
}
/>
</Step>
<Step indicator={<StepIndicator>3</StepIndicator>}>
<Typography>
Result <Chip>{searchResults.length}</Chip>
</Typography>
<MangaList mangas={searchResults} />
</Step>
</Stepper>
</ModalDialog>
</Modal>
);
}

View File

@@ -1,133 +1,231 @@
import {ReactNode, useContext, useState} from "react"; import { ReactNode, useContext, useState } from "react";
import { ApiContext } from "../../apiClient/ApiContext"; import { ApiContext } from "../../apiClient/ApiContext";
import { import {
Button, Button,
Input, Input,
Modal, Modal,
ModalDialog, ModalDialog,
Stack, Stack,
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
Tabs Tabs,
} from "@mui/joy"; } from "@mui/joy";
import ModalClose from "@mui/joy/ModalClose"; import ModalClose from "@mui/joy/ModalClose";
import {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts"; import {
import {LoadingState, StateColor, StateIndicator} from "../Loading.tsx"; GotifyRecord,
NtfyRecord,
PushoverRecord,
} from "../../apiClient/data-contracts.ts";
import { LoadingState, StateColor, StateIndicator } from "../Loading.tsx";
export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) { export default function ({
open,
return ( setOpen,
<Modal open={open} onClose={() => setOpen(false)}> }: {
<ModalDialog> open: boolean;
<ModalClose /> setOpen: (open: boolean) => void;
<Tabs sx={{width:'95%'}} defaultValue={"gotify"}> }) {
<TabList> return (
<Tab value={"gotify"}>Gotify</Tab> <Modal open={open} onClose={() => setOpen(false)}>
<Tab value={"ntfy"}>Ntfy</Tab> <ModalDialog>
<Tab value={"pushover"}>Pushover</Tab> <ModalClose />
</TabList> <Tabs sx={{ width: "95%" }} defaultValue={"gotify"}>
<Gotify /> <TabList>
<Ntfy /> <Tab value={"gotify"}>Gotify</Tab>
<Pushover /> <Tab value={"ntfy"}>Ntfy</Tab>
</Tabs> <Tab value={"pushover"}>Pushover</Tab>
</ModalDialog> </TabList>
</Modal> <Gotify />
); <Ntfy />
<Pushover />
</Tabs>
</ModalDialog>
</Modal>
);
} }
function NotificationConnectorTab({ value, children, add, state }: { value: string, children: ReactNode, add: (data: any) => void, state: LoadingState }) { function NotificationConnectorTab({
value,
const IsLoading = (state : LoadingState) : boolean => state === LoadingState.loading; children,
add,
return ( state,
<TabPanel value={value}> }: {
<Stack spacing={1}> value: string;
{children} children: ReactNode;
<Button onClick={add} endDecorator={StateIndicator(state)} loading={IsLoading(state)} disabled={IsLoading(state)} color={StateColor(state)}>Add</Button> add: (data: any) => void;
</Stack> state: LoadingState;
</TabPanel> }) {
); 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() { function Gotify() {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [gotifyData, setGotifyData] = useState<GotifyRecord>({}); const [gotifyData, setGotifyData] = useState<GotifyRecord>({});
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none); const [loadingState, setLoadingState] = useState<LoadingState>(
LoadingState.none,
);
const Add = () => { const Add = () => {
setLoadingState(LoadingState.loading); setLoadingState(LoadingState.loading);
Api.notificationConnectorGotifyUpdate(gotifyData) Api.notificationConnectorGotifyUpdate(gotifyData)
.then((response) => { .then((response) => {
if (response.ok) if (response.ok) setLoadingState(LoadingState.success);
setLoadingState(LoadingState.success); else setLoadingState(LoadingState.failure);
else })
setLoadingState(LoadingState.failure); .catch((_) => setLoadingState(LoadingState.failure));
}) };
.catch(_ => setLoadingState(LoadingState.failure));
} return (
<NotificationConnectorTab value={"gotify"} add={Add} state={loadingState}>
return ( <Input
<NotificationConnectorTab value={"gotify"} add={Add} state={loadingState}> placeholder={"Name"}
<Input placeholder={"Name"} value={gotifyData.name as string} onChange={(e) => setGotifyData({...gotifyData, name: e.target.value})} /> value={gotifyData.name as string}
<Input placeholder={"https://[...]/message"} value={gotifyData.endpoint as string} onChange={(e) => setGotifyData({...gotifyData, endpoint: e.target.value})} /> onChange={(e) => setGotifyData({ ...gotifyData, name: 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})} /> <Input
</NotificationConnectorTab> 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() { function Ntfy() {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [ntfyData, setNtfyData] = useState<NtfyRecord>({}); const [ntfyData, setNtfyData] = useState<NtfyRecord>({});
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none); const [loadingState, setLoadingState] = useState<LoadingState>(
LoadingState.none,
);
const Add = () => { const Add = () => {
setLoadingState(LoadingState.loading); setLoadingState(LoadingState.loading);
Api.notificationConnectorNtfyUpdate(ntfyData) Api.notificationConnectorNtfyUpdate(ntfyData)
.then((response) => { .then((response) => {
if (response.ok) if (response.ok) setLoadingState(LoadingState.success);
setLoadingState(LoadingState.success); else setLoadingState(LoadingState.failure);
else })
setLoadingState(LoadingState.failure); .catch((_) => setLoadingState(LoadingState.failure));
}) };
.catch(_ => setLoadingState(LoadingState.failure));
}
return ( return (
<NotificationConnectorTab value={"ntfy"} add={Add} state={loadingState}> <NotificationConnectorTab value={"ntfy"} add={Add} state={loadingState}>
<Input placeholder={"Name"} value={ntfyData.name as string} onChange={(e) => setNtfyData({...ntfyData, name: e.target.value})} /> <Input
<Input placeholder={"Endpoint"} value={ntfyData.endpoint as string} onChange={(e) => setNtfyData({...ntfyData, endpoint: e.target.value})} /> placeholder={"Name"}
<Input placeholder={"Topic"} value={ntfyData.topic as string} onChange={(e) => setNtfyData({...ntfyData, topic: e.target.value})} /> value={ntfyData.name as string}
<Input placeholder={"Username"} value={ntfyData.username as string} onChange={(e) => setNtfyData({...ntfyData, username: e.target.value})} /> onChange={(e) => setNtfyData({ ...ntfyData, name: 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})} /> <Input
</NotificationConnectorTab> 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() { function Pushover() {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [pushoverData, setPushoverData] = useState<PushoverRecord>({}); const [pushoverData, setPushoverData] = useState<PushoverRecord>({});
const [loadingState, setLoadingState] = useState<LoadingState>(LoadingState.none); 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 ( const Add = () => {
<NotificationConnectorTab value={"pushover"} add={Add} state={loadingState}> setLoadingState(LoadingState.loading);
<Input placeholder={"Name"} value={pushoverData.name as string} onChange={(e) => setPushoverData({...pushoverData, name: e.target.value})} /> Api.notificationConnectorPushoverUpdate(pushoverData)
<Input placeholder={"User"} value={pushoverData.user as string} onChange={(e) => setPushoverData({...pushoverData, user: e.target.value})} /> .then((response) => {
<Input placeholder={"AppToken"} type={"password"} value={pushoverData.appToken as string} onChange={(e) => setPushoverData({...pushoverData, appToken: e.target.value})} /> if (response.ok) setLoadingState(LoadingState.success);
</NotificationConnectorTab> 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>
);
}

View File

@@ -1,35 +1,43 @@
import {ReactNode, useContext, useState} from "react"; import { ReactNode, useContext, useState } from "react";
import {SettingsContext, SettingsItem} from "./Settings.tsx"; import { SettingsContext, SettingsItem } from "./Settings.tsx";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
import {ColorPaletteProp, Input} from "@mui/joy"; import { ColorPaletteProp, Input } from "@mui/joy";
import * as React from "react"; import * as React from "react";
import MarkdownPreview from "@uiw/react-markdown-preview"; import MarkdownPreview from "@uiw/react-markdown-preview";
export default function () : ReactNode { export default function (): ReactNode {
const settings = useContext(SettingsContext); const settings = useContext(SettingsContext);
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [scheme, setScheme] = useState<ColorPaletteProp>("neutral"); const [scheme, setScheme] = useState<ColorPaletteProp>("neutral");
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined); const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const schemeChanged = (e : React.ChangeEvent<HTMLInputElement>) => { const schemeChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
setScheme("warning"); setScheme("warning");
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
Api.settingsChapterNamingSchemePartialUpdate(e.target.value) Api.settingsChapterNamingSchemePartialUpdate(e.target.value)
.then(response => { .then((response) => {
if (response.ok) if (response.ok) setScheme("success");
setScheme("success"); else setScheme("danger");
else })
setScheme("danger"); .catch(() => setScheme("danger"));
}) }, 1000);
.catch(() => setScheme("danger")); };
}, 1000);
}
return ( return (
<SettingsItem title={"Chapter Naming Scheme"}> <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"} /> <MarkdownPreview
<Input color={scheme} defaultValue={settings?.chapterNamingScheme as string} placeholder={"Scheme"} onChange={schemeChanged} /> style={{ backgroundColor: "transparent" }}
</SettingsItem> 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}
defaultValue={settings?.chapterNamingScheme as string}
placeholder={"Scheme"}
onChange={schemeChanged}
/>
</SettingsItem>
);
}

View File

@@ -1,33 +1,36 @@
import {ReactNode, useContext, useState} from "react"; import { ReactNode, useContext, useState } from "react";
import {SettingsContext, SettingsItem} from "./Settings.tsx"; import { SettingsContext, SettingsItem } from "./Settings.tsx";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
import {ColorPaletteProp, Input} from "@mui/joy"; import { ColorPaletteProp, Input } from "@mui/joy";
import * as React from "react"; import * as React from "react";
export default function () : ReactNode { export default function (): ReactNode {
const settings = useContext(SettingsContext); const settings = useContext(SettingsContext);
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [color, setColor] = useState<ColorPaletteProp>("neutral"); const [color, setColor] = useState<ColorPaletteProp>("neutral");
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined); const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const languageChanged = (e : React.ChangeEvent<HTMLInputElement>) => { const languageChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
setColor("warning"); setColor("warning");
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
Api.settingsDownloadLanguagePartialUpdate(e.target.value) Api.settingsDownloadLanguagePartialUpdate(e.target.value)
.then(response => { .then((response) => {
if (response.ok) if (response.ok) setColor("success");
setColor("success"); else setColor("danger");
else })
setColor("danger"); .catch(() => setColor("danger"));
}) }, 1000);
.catch(() => setColor("danger")); };
}, 1000);
}
return ( return (
<SettingsItem title={"Download Language"}> <SettingsItem title={"Download Language"}>
<Input color={color} defaultValue={settings?.downloadLanguage as string} placeholder={"Language code (f.e. 'en')"} onChange={languageChanged} /> <Input
</SettingsItem> color={color}
); defaultValue={settings?.downloadLanguage as string}
} placeholder={"Language code (f.e. 'en')"}
onChange={languageChanged}
/>
</SettingsItem>
);
}

View File

@@ -1,33 +1,37 @@
import {ReactNode, useContext, useState} from "react"; import { ReactNode, useContext, useState } from "react";
import {SettingsContext, SettingsItem} from "./Settings.tsx"; import { SettingsContext, SettingsItem } from "./Settings.tsx";
import {ColorPaletteProp, Input} from "@mui/joy"; import { ColorPaletteProp, Input } from "@mui/joy";
import * as React from "react"; import * as React from "react";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
export default function () : ReactNode { export default function (): ReactNode {
const settings = useContext(SettingsContext); const settings = useContext(SettingsContext);
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [uriColor, setUriColor] = useState<ColorPaletteProp>("neutral"); const [uriColor, setUriColor] = useState<ColorPaletteProp>("neutral");
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined); const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
const uriChanged = (e : React.ChangeEvent<HTMLInputElement>) => { const uriChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
clearTimeout(timerRef.current); clearTimeout(timerRef.current);
setUriColor("warning"); setUriColor("warning");
timerRef.current = setTimeout(() => { timerRef.current = setTimeout(() => {
Api.settingsFlareSolverrUrlCreate(e.target.value) Api.settingsFlareSolverrUrlCreate(e.target.value)
.then(response => { .then((response) => {
if (response.ok) if (response.ok) setUriColor("success");
setUriColor("success"); else setUriColor("danger");
else })
setUriColor("danger"); .catch(() => setUriColor("danger"));
}) }, 1000);
.catch(() => setUriColor("danger")); };
}, 1000);
} return (
<SettingsItem title={"FlareSolverr"}>
return ( <Input
<SettingsItem title={"FlareSolverr"}> color={uriColor}
<Input color={uriColor} defaultValue={settings?.flareSolverrUrl as string} type={"url"} placeholder={"URL"} onChange={uriChanged} /> defaultValue={settings?.flareSolverrUrl as string}
</SettingsItem> type={"url"}
); placeholder={"URL"}
} onChange={uriChanged}
/>
</SettingsItem>
);
}

View File

@@ -1,13 +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 (): ReactNode {
const settings = useContext(SettingsContext); const settings = useContext(SettingsContext);
return ( return (
<SettingsItem title={"Image Compression"}> <SettingsItem title={"Image Compression"}>
<Slider sx={{marginTop: "20px"}} valueLabelDisplay={"auto"} defaultValue={settings?.imageCompression}></Slider> <Slider
</SettingsItem> sx={{ marginTop: "20px" }}
); valueLabelDisplay={"auto"}
} defaultValue={settings?.imageCompression}
></Slider>
</SettingsItem>
);
}

View File

@@ -1,33 +1,33 @@
import {Button} from "@mui/joy"; import { Button } from "@mui/joy";
import {SettingsItem} from "./Settings.tsx"; import { SettingsItem } from "./Settings.tsx";
import {useContext, useState} from "react"; import { useContext, useState } from "react";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import { ApiContext } from "../../apiClient/ApiContext.tsx";
import {LoadingState, StateColor, StateIndicator} from "../Loading.tsx"; import { LoadingState, StateColor, StateIndicator } from "../Loading.tsx";
export default function () { export default function () {
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [unusedMangaState, setUnusedMangaState] = useState(LoadingState.none);
const cleanUnusedManga = () => {
setUnusedMangaState(LoadingState.loading);
Api.maintenanceCleanupNoDownloadMangaCreate()
.then(r => {
if (r.ok)
setUnusedMangaState(LoadingState.success);
else
setUnusedMangaState(LoadingState.failure);
}).catch(_ => setUnusedMangaState(LoadingState.failure));
}
return (
<SettingsItem title={"Maintenance"}> const [unusedMangaState, setUnusedMangaState] = useState(LoadingState.none);
<Button const cleanUnusedManga = () => {
disabled={unusedMangaState == LoadingState.loading} setUnusedMangaState(LoadingState.loading);
color={StateColor(unusedMangaState)} Api.maintenanceCleanupNoDownloadMangaCreate()
endDecorator={StateIndicator(unusedMangaState)} .then((r) => {
onClick={cleanUnusedManga}>Cleanup unused Manga</Button> if (r.ok) setUnusedMangaState(LoadingState.success);
</SettingsItem> else setUnusedMangaState(LoadingState.failure);
); })
} .catch((_) => setUnusedMangaState(LoadingState.failure));
};
return (
<SettingsItem title={"Maintenance"}>
<Button
disabled={unusedMangaState == LoadingState.loading}
color={StateColor(unusedMangaState)}
endDecorator={StateIndicator(unusedMangaState)}
onClick={cleanUnusedManga}
>
Cleanup unused Manga
</Button>
</SettingsItem>
);
}

View File

@@ -1,16 +1,22 @@
import {ReactNode} from "react"; import { ReactNode } from "react";
import {SettingsItem} from "./Settings.tsx"; import { SettingsItem } from "./Settings.tsx";
import {Button} from "@mui/joy"; import { Button } from "@mui/joy";
import NotificationConnectors from "./AddNotificationConnector.tsx"; import NotificationConnectors from "./AddNotificationConnector.tsx";
import * as React from "react"; import * as React from "react";
export default function () : ReactNode { export default function (): ReactNode {
const [notificationConnectorsOpen, setNotificationConnectorsOpen] = React.useState(false); const [notificationConnectorsOpen, setNotificationConnectorsOpen] =
React.useState(false);
return (
<SettingsItem title={"Notification Connectors"}> return (
<Button onClick={() => setNotificationConnectorsOpen(true)}>Add Notification Connector</Button> <SettingsItem title={"Notification Connectors"}>
<NotificationConnectors open={notificationConnectorsOpen} setOpen={setNotificationConnectorsOpen} /> <Button onClick={() => setNotificationConnectorsOpen(true)}>
</SettingsItem> Add Notification Connector
); </Button>
} <NotificationConnectors
open={notificationConnectorsOpen}
setOpen={setNotificationConnectorsOpen}
/>
</SettingsItem>
);
}

View File

@@ -1,95 +1,117 @@
import ModalClose from '@mui/joy/ModalClose'; import ModalClose from "@mui/joy/ModalClose";
import { import {
Accordion, Accordion,
AccordionDetails, AccordionDetails,
AccordionGroup, AccordionGroup,
AccordionSummary, Button, ColorPaletteProp, AccordionSummary,
DialogContent, Button,
DialogTitle, Input, ColorPaletteProp,
Modal, ModalDialog DialogContent,
DialogTitle,
Input,
Modal,
ModalDialog,
} from "@mui/joy"; } from "@mui/joy";
import './Settings.css'; import "./Settings.css";
import * as React from "react"; import * as React from "react";
import {createContext, Dispatch, ReactNode, useContext, useEffect, useState} from "react"; import {
import {TrangaSettings} from "../../apiClient/data-contracts.ts"; createContext,
import {ApiContext} from "../../apiClient/ApiContext.tsx"; Dispatch,
ReactNode,
useContext,
useEffect,
useState,
} from "react";
import { TrangaSettings } from "../../apiClient/data-contracts.ts";
import { ApiContext } from "../../apiClient/ApiContext.tsx";
import NotificationConnectors from "./NotificationConnectors.tsx"; import NotificationConnectors from "./NotificationConnectors.tsx";
import {SxProps} from "@mui/joy/styles/types"; import { SxProps } from "@mui/joy/styles/types";
import ImageCompression from "./ImageCompression.tsx"; import ImageCompression from "./ImageCompression.tsx";
import FlareSolverr from "./FlareSolverr.tsx"; import FlareSolverr from "./FlareSolverr.tsx";
import DownloadLanguage from "./DownloadLanguage.tsx"; import DownloadLanguage from "./DownloadLanguage.tsx";
import ChapterNamingScheme from "./ChapterNamingScheme.tsx"; import ChapterNamingScheme from "./ChapterNamingScheme.tsx";
import Maintenance from "./Maintenance.tsx"; import Maintenance from "./Maintenance.tsx";
export const SettingsContext = createContext<TrangaSettings|undefined>(undefined); export const SettingsContext = createContext<TrangaSettings | undefined>(
undefined,
);
export default function Settings({setApiUri} : {setApiUri: Dispatch<React.SetStateAction<string>>}) { export default function Settings({
const Api = useContext(ApiContext); setApiUri,
const [settings, setSettings] = useState<TrangaSettings>(); }: {
setApiUri: Dispatch<React.SetStateAction<string>>;
}) {
const Api = useContext(ApiContext);
const [settings, setSettings] = useState<TrangaSettings>();
const [open, setOpen] = React.useState(false); const [open, setOpen] = React.useState(false);
const [apiUriColor, setApiUriColor] = useState<ColorPaletteProp>("neutral");
const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
useEffect(() => { const [apiUriColor, setApiUriColor] = useState<ColorPaletteProp>("neutral");
Api.settingsList().then((response) => { const timerRef = React.useRef<ReturnType<typeof setTimeout>>(undefined);
setSettings(response.data);
});
}, []);
const apiUriChanged = (e : React.ChangeEvent<HTMLInputElement>) => { useEffect(() => {
clearTimeout(timerRef.current); Api.settingsList().then((response) => {
setApiUriColor("warning"); setSettings(response.data);
timerRef.current = setTimeout(() => { });
setApiUri(e.target.value); }, []);
setApiUriColor("success");
}, 1000); const apiUriChanged = (e: React.ChangeEvent<HTMLInputElement>) => {
} clearTimeout(timerRef.current);
setApiUriColor("warning");
const ModalStyle : SxProps = { timerRef.current = setTimeout(() => {
width: "80%", setApiUri(e.target.value);
height: "80%" setApiUriColor("success");
} }, 1000);
};
return (
<SettingsContext.Provider value={settings}> const ModalStyle: SxProps = {
<Button onClick={() => setOpen(true)}>Settings</Button> width: "80%",
<Modal open={open} onClose={() => setOpen(false)}> height: "80%",
<ModalDialog sx={ModalStyle}> };
<ModalClose />
<DialogTitle>Settings</DialogTitle> return (
<DialogContent> <SettingsContext.Provider value={settings}>
<AccordionGroup> <Button onClick={() => setOpen(true)}>Settings</Button>
<SettingsItem title={"ApiUri"}> <Modal open={open} onClose={() => setOpen(false)}>
<Input <ModalDialog sx={ModalStyle}>
color={apiUriColor} <ModalClose />
placeholder={"http(s)://"} <DialogTitle>Settings</DialogTitle>
type={"url"} <DialogContent>
defaultValue={Api.baseUrl} <AccordionGroup>
onChange={apiUriChanged} /> <SettingsItem title={"ApiUri"}>
</SettingsItem> <Input
<ImageCompression /> color={apiUriColor}
<FlareSolverr /> placeholder={"http(s)://"}
<DownloadLanguage /> type={"url"}
<ChapterNamingScheme /> defaultValue={Api.baseUrl}
<NotificationConnectors /> onChange={apiUriChanged}
<Maintenance /> />
</AccordionGroup> </SettingsItem>
</DialogContent> <ImageCompression />
</ModalDialog> <FlareSolverr />
</Modal> <DownloadLanguage />
</SettingsContext.Provider> <ChapterNamingScheme />
); <NotificationConnectors />
<Maintenance />
</AccordionGroup>
</DialogContent>
</ModalDialog>
</Modal>
</SettingsContext.Provider>
);
} }
export function SettingsItem({title, children} : {title: string, children: ReactNode}) { export function SettingsItem({
return ( title,
<Accordion> children,
<AccordionSummary>{title}</AccordionSummary> }: {
<AccordionDetails> title: string;
{children} children: ReactNode;
</AccordionDetails> }) {
</Accordion> return (
); <Accordion>
} <AccordionSummary>{title}</AccordionSummary>
<AccordionDetails>{children}</AccordionDetails>
</Accordion>
);
}

View File

@@ -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;
} }

View File

@@ -1,35 +1,91 @@
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, useContext} from "react"; import { ReactElement, 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 "./apiClient/ApiContext.tsx";
export default function Header({children} : {children? : ReactElement<any, any> | ReactElement<any,any>[] | undefined}) : ReactElement { export default function Header({
const Api = useContext(ApiContext); children,
}: {
return ( children?: ReactElement<any, any> | ReactElement<any, any>[] | undefined;
<Sheet className={"header"}> }): ReactElement {
<Stack direction={"row"} spacing={2} sx={{width: "100%", alignItems: "center", justifyContent: "space-between"}} useFlexGap> const Api = useContext(ApiContext);
<Stack sx={{flexGrow: 1, flexBasis: 1}} direction={"row"} spacing={2}>
{children} return (
</Stack> <Sheet className={"header"}>
<Stack sx={{flexGrow: 1, height: "100%", flexBasis: 1, justifyContent: "center"}} direction={"row"}> <Stack
<img src={"/blahaj.png"} style={{cursor: "grab", maxHeight: "100%"}}/> direction={"row"}
<Typography level={"h2"} sx={{ spacing={2}
background: "linear-gradient(110deg, var(--joy-palette-primary-solidBg), var(--joy-palette-success-400))", sx={{
WebkitBackgroundClip: "text", width: "100%",
WebkitTextFillColor: "transparent", alignItems: "center",
fontWeight: "bold", justifyContent: "space-between",
cursor: "default" }}
}}>Tranga</Typography> useFlexGap
</Stack> >
<Stack sx={{flexGrow: 1, flexBasis: 1, justifyContent: "flex-end"}} direction={"row"} spacing={2}> <Stack sx={{ flexGrow: 1, flexBasis: 1 }} direction={"row"} spacing={2}>
<Link target={"_blank"} href={"https://github.com/C9Glax/tranga"} color={"neutral"} height={"min-content"} ><GitHub /> Server</Link> {children}
<Link target={"_blank"} href={"https://github.com/C9Glax/tranga-website"} color={"neutral"} height={"min-content"} ><GitHub /> Website</Link> </Stack>
<Link target={"_blank"} href={Api.baseUrl + "/swagger"} color={"neutral"} height={"min-content"} ><Article />Swagger</Link> <Stack
</Stack> sx={{
</Stack> flexGrow: 1,
</Sheet> height: "100%",
); flexBasis: 1,
} justifyContent: "center",
}}
direction={"row"}
>
<img
src={"/blahaj.png"}
style={{ cursor: "grab", maxHeight: "100%" }}
/>
<Typography
level={"h2"}
sx={{
background:
"linear-gradient(110deg, var(--joy-palette-primary-solidBg), var(--joy-palette-success-400))",
WebkitBackgroundClip: "text",
WebkitTextFillColor: "transparent",
fontWeight: "bold",
cursor: "default",
}}
>
Tranga
</Typography>
</Stack>
<Stack
sx={{ flexGrow: 1, flexBasis: 1, justifyContent: "flex-end" }}
direction={"row"}
spacing={2}
>
<Link
target={"_blank"}
href={"https://github.com/C9Glax/tranga"}
color={"neutral"}
height={"min-content"}
>
<GitHub /> Server
</Link>
<Link
target={"_blank"}
href={"https://github.com/C9Glax/tranga-website"}
color={"neutral"}
height={"min-content"}
>
<GitHub /> Website
</Link>
<Link
target={"_blank"}
href={Api.baseUrl + "/swagger"}
color={"neutral"}
height={"min-content"}
>
<Article />
Swagger
</Link>
</Stack>
</Stack>
</Sheet>
);
}

View File

@@ -1,4 +1,4 @@
import { createContext } from "react"; import { createContext } from "react";
import {V2} from "./V2.ts"; import { V2 } from "./V2.ts";
export const ApiContext = createContext<V2>(new V2()); export const ApiContext = createContext<V2>(new V2());

View File

@@ -1,31 +1,32 @@
import {createContext, useContext, useState} from "react"; import { createContext, useContext, useState } from "react";
import {TrangaSettings} from "./data-contracts.ts"; import { TrangaSettings } from "./data-contracts.ts";
import {ApiContext} from "./ApiContext.tsx"; import { ApiContext } from "./ApiContext.tsx";
const [settingsPromise, setSettingsPromise] = useState<Promise<TrangaSettings | undefined>>(); const [settingsPromise, setSettingsPromise] =
useState<Promise<TrangaSettings | undefined>>();
const [settings, setSettings] = useState<TrangaSettings>(); const [settings, setSettings] = useState<TrangaSettings>();
const API = useContext(ApiContext); const API = useContext(ApiContext);
export const SettingsContext = createContext<{ GetSettings: () => Promise<TrangaSettings | undefined> }>( export const SettingsContext = createContext<{
{ GetSettings: () => Promise<TrangaSettings | undefined>;
GetSettings: () : Promise<TrangaSettings | undefined> => { }>({
const promise = settingsPromise; GetSettings: (): Promise<TrangaSettings | undefined> => {
if(promise) return promise; const promise = settingsPromise;
const p = new Promise<TrangaSettings | undefined>((resolve, reject) => { if (promise) return promise;
if (settings) resolve(settings); const p = new Promise<TrangaSettings | undefined>((resolve, reject) => {
if (settings) resolve(settings);
console.log(`Fetching settings`); console.log(`Fetching settings`);
API.settingsList() API.settingsList()
.then(result => { .then((result) => {
if (!result.ok) if (!result.ok) throw new Error(`Error fetching settings`);
throw new Error(`Error fetching settings`); setSettings(result.data);
setSettings(result.data); resolve(result.data);
resolve(result.data); })
}).catch(reject); .catch(reject);
}); });
setSettingsPromise(p); setSettingsPromise(p);
return p; return p;
} },
} });
);

View File

@@ -1,29 +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-ignore // @ts-ignore
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 />
);

View File

@@ -1,88 +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)"
}
}
}, },
"dark": { neutral: {
"palette": {} "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)",
},
},
},
dark: {
palette: {},
},
},
});

View File

@@ -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",
} },
}) });