Reorganize Settings for faster access to individual settings (group them by category)

This commit is contained in:
2025-08-04 11:39:02 +02:00
parent d5975192e8
commit ee1b0536c7
10 changed files with 104 additions and 77 deletions

View File

@@ -1,7 +1,7 @@
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, Card,
Input, Input,
Modal, Modal,
ModalDialog, ModalDialog,
@@ -9,30 +9,36 @@ import {
Tab, Tab,
TabList, TabList,
TabPanel, TabPanel,
Tabs Tabs, Typography
} 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 {GotifyRecord, NtfyRecord, PushoverRecord} from "../../apiClient/data-contracts.ts";
import {LoadingState, StateColor, StateIndicator} from "../Loading.tsx"; import {LoadingState, StateColor, StateIndicator} from "../Loading.tsx";
import * as React from "react";
export default function ({open, setOpen} : {open: boolean, setOpen: (open: boolean) => void}) { export default function () {
const [notificationConnectorsOpen, setNotificationConnectorsOpen] = React.useState(false);
return ( return (
<Modal open={open} onClose={() => setOpen(false)}> <Card>
<ModalDialog> <Typography>Notification Connectors</Typography>
<ModalClose /> <Button onClick={() => setNotificationConnectorsOpen(true)}>Add</Button>
<Tabs sx={{width:'95%'}} defaultValue={"gotify"}> <Modal open={notificationConnectorsOpen} onClose={() => setNotificationConnectorsOpen(false)}>
<TabList> <ModalDialog>
<Tab value={"gotify"}>Gotify</Tab> <ModalClose />
<Tab value={"ntfy"}>Ntfy</Tab> <Tabs sx={{width:'95%'}} defaultValue={"gotify"}>
<Tab value={"pushover"}>Pushover</Tab> <TabList>
</TabList> <Tab value={"gotify"}>Gotify</Tab>
<Gotify /> <Tab value={"ntfy"}>Ntfy</Tab>
<Ntfy /> <Tab value={"pushover"}>Pushover</Tab>
<Pushover /> </TabList>
</Tabs> <Gotify />
</ModalDialog> <Ntfy />
</Modal> <Pushover />
</Tabs>
</ModalDialog>
</Modal>
</Card>
); );
} }

View File

@@ -1,7 +1,7 @@
import {ReactNode, useContext, useState} from "react"; import {ReactNode, useContext, useState} from "react";
import {SettingsContext, SettingsItem} from "./Settings.tsx"; import {SettingsContext} from "./Settings.tsx";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {ApiContext} from "../../apiClient/ApiContext.tsx";
import {ColorPaletteProp, Input} from "@mui/joy"; import {Card, ColorPaletteProp, Input, Typography} 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";
@@ -27,9 +27,10 @@ export default function () : ReactNode {
} }
return ( return (
<SettingsItem title={"Chapter Naming Scheme"}> <Card>
<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"} /> <Typography>Chapter Naming Scheme</Typography>
<Input color={scheme} defaultValue={settings?.chapterNamingScheme as string} placeholder={"Scheme"} onChange={schemeChanged} /> <Input color={scheme} defaultValue={settings?.chapterNamingScheme as string} placeholder={"Scheme"} onChange={schemeChanged} />
</SettingsItem> <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"} />
</Card>
); );
} }

View File

@@ -0,0 +1,14 @@
import {SettingsItem} from "./Settings.tsx";
import ImageCompression from "./ImageCompression.tsx";
import DownloadLanguage from "./DownloadLanguage.tsx";
import ChapterNamingScheme from "./ChapterNamingScheme.tsx";
export default function (){
return (
<SettingsItem title={"Download"}>
<ImageCompression />
<DownloadLanguage />
<ChapterNamingScheme />
</SettingsItem>
)
}

View File

@@ -1,7 +1,7 @@
import {ReactNode, useContext, useState} from "react"; import {ReactNode, useContext, useState} from "react";
import {SettingsContext, SettingsItem} from "./Settings.tsx"; import {SettingsContext} from "./Settings.tsx";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {ApiContext} from "../../apiClient/ApiContext.tsx";
import {ColorPaletteProp, Input} from "@mui/joy"; import {Card, ColorPaletteProp, Input, Typography} from "@mui/joy";
import * as React from "react"; import * as React from "react";
export default function () : ReactNode { export default function () : ReactNode {
@@ -26,8 +26,9 @@ export default function () : ReactNode {
} }
return ( return (
<SettingsItem title={"Download Language"}> <Card>
<Typography>Download Language</Typography>
<Input color={color} defaultValue={settings?.downloadLanguage as string} placeholder={"Language code (f.e. 'en')"} onChange={languageChanged} /> <Input color={color} defaultValue={settings?.downloadLanguage as string} placeholder={"Language code (f.e. 'en')"} onChange={languageChanged} />
</SettingsItem> </Card>
); );
} }

View File

@@ -1,6 +1,6 @@
import {ReactNode, useContext, useState} from "react"; import {ReactNode, useCallback, useContext, useState} from "react";
import {SettingsContext, SettingsItem} from "./Settings.tsx"; import {SettingsContext} from "./Settings.tsx";
import {ColorPaletteProp, Input} from "@mui/joy"; import {Button, Card, ColorPaletteProp, Input, Typography} 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";
@@ -8,26 +8,34 @@ export default function () : ReactNode {
const settings = useContext(SettingsContext); const settings = useContext(SettingsContext);
const Api = useContext(ApiContext); const Api = useContext(ApiContext);
const [uriValue, setUriValue] = React.useState<string>(settings?.flareSolverrUrl as string);
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) setUriValue(e.target.value);
.then(response => {
if (response.ok)
setUriColor("success");
else
setUriColor("danger");
})
.catch(() => setUriColor("danger"));
}, 1000); }, 1000);
} }
const changeUri = useCallback(() => {
Api.settingsFlareSolverrUrlCreate(uriValue)
.then(response => {
if (response.ok)
setUriColor("success");
else
setUriColor("danger");
})
.catch(() => setUriColor("danger"));
}, [uriValue]);
return ( return (
<SettingsItem title={"FlareSolverr"}> <Card>
<Input color={uriColor} defaultValue={settings?.flareSolverrUrl as string} type={"url"} placeholder={"URL"} onChange={uriChanged} /> <Typography>FlareSolverr</Typography>
</SettingsItem> <Input color={uriColor} value={uriValue} type={"url"} placeholder={"URL"} onChange={uriChanged}
endDecorator={<Button onClick={changeUri}>Apply</Button>}
/>
</Card>
); );
} }

View File

@@ -1,13 +1,14 @@
import {ReactNode, useContext} from "react"; import {ReactNode, useContext} from "react";
import {SettingsContext, SettingsItem} from "./Settings.tsx"; import {SettingsContext} from "./Settings.tsx";
import {Slider} from "@mui/joy"; import {Card, Slider, Typography} 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"}> <Card>
<Typography>Image Compression</Typography>
<Slider sx={{marginTop: "20px"}} valueLabelDisplay={"auto"} defaultValue={settings?.imageCompression}></Slider> <Slider sx={{marginTop: "20px"}} valueLabelDisplay={"auto"} defaultValue={settings?.imageCompression}></Slider>
</SettingsItem> </Card>
); );
} }

View File

@@ -19,9 +19,7 @@ export default function () {
}).catch(_ => setUnusedMangaState(LoadingState.failure)); }).catch(_ => setUnusedMangaState(LoadingState.failure));
} }
return ( return (
<SettingsItem title={"Maintenance"}> <SettingsItem title={"Maintenance"}>
<Button <Button
disabled={unusedMangaState == LoadingState.loading} disabled={unusedMangaState == LoadingState.loading}

View File

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

View File

@@ -0,0 +1,15 @@
import {SettingsItem} from "./Settings.tsx";
import NotificationConnectors from "./AddNotificationConnector.tsx";
import FlareSolverr from "./FlareSolverr.tsx";
export default function(){
return (
<SettingsItem title={"Services"} direction={"row"}>
<FlareSolverr />
<NotificationConnectors />
</SettingsItem>
);
}

View File

@@ -6,20 +6,17 @@ import {
AccordionSummary, Button, ColorPaletteProp, AccordionSummary, Button, ColorPaletteProp,
DialogContent, DialogContent,
DialogTitle, Input, DialogTitle, Input,
Modal, ModalDialog Modal, ModalDialog, Stack
} 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 {createContext, Dispatch, ReactNode, useContext, useEffect, useState} from "react";
import {TrangaSettings} from "../../apiClient/data-contracts.ts"; import {TrangaSettings} from "../../apiClient/data-contracts.ts";
import {ApiContext} from "../../apiClient/ApiContext.tsx"; import {ApiContext} from "../../apiClient/ApiContext.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 FlareSolverr from "./FlareSolverr.tsx";
import DownloadLanguage from "./DownloadLanguage.tsx";
import ChapterNamingScheme from "./ChapterNamingScheme.tsx";
import Maintenance from "./Maintenance.tsx"; import Maintenance from "./Maintenance.tsx";
import Services from "./Services.tsx";
import Download from './Download.tsx';
export const SettingsContext = createContext<TrangaSettings|undefined>(undefined); export const SettingsContext = createContext<TrangaSettings|undefined>(undefined);
@@ -69,11 +66,8 @@ export default function Settings({setApiUri} : {setApiUri: Dispatch<React.SetSta
defaultValue={Api.baseUrl} defaultValue={Api.baseUrl}
onChange={apiUriChanged} /> onChange={apiUriChanged} />
</SettingsItem> </SettingsItem>
<ImageCompression /> <Download />
<FlareSolverr /> <Services />
<DownloadLanguage />
<ChapterNamingScheme />
<NotificationConnectors />
<Maintenance /> <Maintenance />
</AccordionGroup> </AccordionGroup>
</DialogContent> </DialogContent>
@@ -83,12 +77,17 @@ export default function Settings({setApiUri} : {setApiUri: Dispatch<React.SetSta
); );
} }
export function SettingsItem({title, children} : {title: string, children: ReactNode}) { export function SettingsItem({title, children, defaultExpanded, direction} : {title: string, children?: ReactNode, defaultExpanded?: boolean, direction?: "row" | "column"}) {
const [expanded, setExpanded] = React.useState(defaultExpanded??false);
const stackDirection = direction ?? "column";
return ( return (
<Accordion> <Accordion expanded={expanded} onChange={(_, expanded) => setExpanded(expanded)}>
<AccordionSummary>{title}</AccordionSummary> <AccordionSummary>{title}</AccordionSummary>
<AccordionDetails> <AccordionDetails>
{children} <Stack direction={stackDirection} spacing={1}>
{children}
</Stack>
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
); );