mirror of
https://github.com/morpheus65535/bazarr.git
synced 2025-04-24 22:57:13 -04:00
Fix #1872, refactor the settings builder
This commit is contained in:
parent
d2b40bd781
commit
d9c334d43a
19 changed files with 252 additions and 301 deletions
|
@ -21,6 +21,7 @@ import {
|
|||
Selector,
|
||||
Text,
|
||||
} from "../components";
|
||||
import { BaseUrlModification } from "../utilities/modifications";
|
||||
import { branchOptions, proxyOptions, securityOptions } from "./options";
|
||||
|
||||
const characters = "abcdef0123456789";
|
||||
|
@ -33,9 +34,6 @@ const generateApiKey = () => {
|
|||
.join("");
|
||||
};
|
||||
|
||||
const baseUrlOverride = (settings: Settings) =>
|
||||
settings.general.base_url?.slice(1) ?? "";
|
||||
|
||||
const SettingsGeneralView: FunctionComponent = () => {
|
||||
const [copied, setCopy] = useState(false);
|
||||
|
||||
|
@ -59,8 +57,10 @@ const SettingsGeneralView: FunctionComponent = () => {
|
|||
label="Base URL"
|
||||
icon="/"
|
||||
settingKey="settings-general-base_url"
|
||||
override={baseUrlOverride}
|
||||
beforeStaged={(v) => "/" + v}
|
||||
settingOptions={{
|
||||
onLoaded: BaseUrlModification,
|
||||
onSubmit: (v) => "/" + v,
|
||||
}}
|
||||
></Text>
|
||||
<Message>Reverse proxy support</Message>
|
||||
</Section>
|
||||
|
@ -71,7 +71,9 @@ const SettingsGeneralView: FunctionComponent = () => {
|
|||
options={securityOptions}
|
||||
placeholder="No Authentication"
|
||||
settingKey="settings-auth-type"
|
||||
beforeStaged={(v) => (v === null ? "None" : v)}
|
||||
settingOptions={{
|
||||
onSubmit: (v) => (v === null ? "None" : v),
|
||||
}}
|
||||
></Selector>
|
||||
<CollapseBox settingKey="settings-auth-type">
|
||||
<Text label="Username" settingKey="settings-auth-username"></Text>
|
||||
|
@ -121,7 +123,9 @@ const SettingsGeneralView: FunctionComponent = () => {
|
|||
settingKey="settings-proxy-type"
|
||||
placeholder="No Proxy"
|
||||
options={proxyOptions}
|
||||
beforeStaged={(v) => (v === null ? "None" : v)}
|
||||
settingOptions={{
|
||||
onSubmit: (v) => (v === null ? "None" : v),
|
||||
}}
|
||||
></Selector>
|
||||
<CollapseBox
|
||||
settingKey="settings-proxy-type"
|
||||
|
|
|
@ -8,8 +8,9 @@ import { useSelectorOptions } from "@/utilities";
|
|||
import { InputWrapper } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { useLatestEnabledLanguages, useLatestProfiles } from ".";
|
||||
import { BaseInput, Selector, SelectorProps } from "../components";
|
||||
import { Selector, SelectorProps } from "../components";
|
||||
import { useFormActions } from "../utilities/FormValues";
|
||||
import { BaseInput } from "../utilities/hooks";
|
||||
|
||||
type LanguageSelectorProps = Omit<
|
||||
MultiSelectorProps<Language.Info>,
|
||||
|
@ -41,7 +42,7 @@ export const LanguageSelector: FunctionComponent<
|
|||
};
|
||||
|
||||
export const ProfileSelector: FunctionComponent<
|
||||
Omit<SelectorProps<number>, "beforeStaged" | "options" | "clearable">
|
||||
Omit<SelectorProps<number>, "settingOptions" | "options" | "clearable">
|
||||
> = ({ ...props }) => {
|
||||
const profiles = useLatestProfiles();
|
||||
|
||||
|
@ -58,7 +59,7 @@ export const ProfileSelector: FunctionComponent<
|
|||
{...props}
|
||||
clearable
|
||||
options={profileOptions}
|
||||
beforeStaged={(v) => (v === null ? "" : v)}
|
||||
settingOptions={{ onSubmit: (v) => (v === null ? "" : v) }}
|
||||
></Selector>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,15 +1,9 @@
|
|||
import { useLanguageProfiles, useLanguages } from "@/apis/hooks";
|
||||
import { useEnabledLanguages } from "@/utilities/languages";
|
||||
import { FunctionComponent } from "react";
|
||||
import {
|
||||
Check,
|
||||
CollapseBox,
|
||||
Layout,
|
||||
Message,
|
||||
Section,
|
||||
useSettingValue,
|
||||
} from "../components";
|
||||
import { Check, CollapseBox, Layout, Message, Section } from "../components";
|
||||
import { enabledLanguageKey, languageProfileKey } from "../keys";
|
||||
import { useSettingValue } from "../utilities/hooks";
|
||||
import { LanguageSelector, ProfileSelector } from "./components";
|
||||
import Table from "./table";
|
||||
|
||||
|
|
|
@ -14,8 +14,9 @@ import {
|
|||
import { useForm } from "@mantine/hooks";
|
||||
import { FunctionComponent, useMemo } from "react";
|
||||
import { useMutation } from "react-query";
|
||||
import { Card, useLatestArray, useUpdateArray } from "../components";
|
||||
import { Card } from "../components";
|
||||
import { notificationsKey } from "../keys";
|
||||
import { useSettingValue, useUpdateArray } from "../utilities/hooks";
|
||||
|
||||
interface Props {
|
||||
selections: readonly Settings.NotificationInfo[];
|
||||
|
@ -100,10 +101,12 @@ const NotificationModal = withModal(NotificationForm, "notification-tool", {
|
|||
});
|
||||
|
||||
export const NotificationView: FunctionComponent = () => {
|
||||
const notifications = useLatestArray<Settings.NotificationInfo>(
|
||||
const notifications = useSettingValue<Settings.NotificationInfo[]>(
|
||||
notificationsKey,
|
||||
"name",
|
||||
(s) => s.notifications.providers
|
||||
{
|
||||
onLoaded: (settings) => settings.notifications.providers,
|
||||
onSubmit: (value) => value.map((v) => JSON.stringify(v)),
|
||||
}
|
||||
);
|
||||
|
||||
const update = useUpdateArray<Settings.NotificationInfo>(
|
||||
|
|
|
@ -20,21 +20,14 @@ import {
|
|||
useRef,
|
||||
useState,
|
||||
} from "react";
|
||||
import {
|
||||
Card,
|
||||
Check,
|
||||
Chips,
|
||||
Message,
|
||||
Password,
|
||||
Text,
|
||||
useSettingValue,
|
||||
} from "../components";
|
||||
import { Card, Check, Chips, Message, Password, Text } from "../components";
|
||||
import {
|
||||
FormContext,
|
||||
FormValues,
|
||||
useFormActions,
|
||||
useStagedValues,
|
||||
} from "../utilities/FormValues";
|
||||
import { useSettingValue } from "../utilities/hooks";
|
||||
import { SettingsProvider, useSettings } from "../utilities/SettingsProvider";
|
||||
import { ProviderInfo, ProviderList } from "./list";
|
||||
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Code } from "@mantine/core";
|
||||
import { FunctionComponent, useCallback } from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import {
|
||||
Check,
|
||||
Chips,
|
||||
|
@ -14,12 +14,9 @@ import {
|
|||
URLTestButton,
|
||||
} from "../components";
|
||||
import { moviesEnabledKey } from "../keys";
|
||||
import { BaseUrlModification } from "../utilities/modifications";
|
||||
|
||||
const SettingsRadarrView: FunctionComponent = () => {
|
||||
const baseUrlOverride = useCallback((settings: Settings) => {
|
||||
return settings.radarr.base_url?.slice(1) ?? "";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout name="Radarr">
|
||||
<Section header="Use Radarr">
|
||||
|
@ -34,8 +31,10 @@ const SettingsRadarrView: FunctionComponent = () => {
|
|||
label="Base URL"
|
||||
icon="/"
|
||||
settingKey="settings-radarr-base_url"
|
||||
override={baseUrlOverride}
|
||||
beforeStaged={(v) => "/" + v}
|
||||
settingOptions={{
|
||||
onLoaded: BaseUrlModification,
|
||||
onSubmit: (v) => "/" + v,
|
||||
}}
|
||||
></Text>
|
||||
<Text label="API Key" settingKey="settings-radarr-apikey"></Text>
|
||||
<Check label="SSL" settingKey="settings-radarr-ssl"></Check>
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { Code } from "@mantine/core";
|
||||
import { FunctionComponent, useCallback } from "react";
|
||||
import { FunctionComponent } from "react";
|
||||
import {
|
||||
Check,
|
||||
Chips,
|
||||
|
@ -16,12 +16,9 @@ import {
|
|||
} from "../components";
|
||||
import { seriesEnabledKey } from "../keys";
|
||||
import { seriesTypeOptions } from "../options";
|
||||
import { BaseUrlModification } from "../utilities/modifications";
|
||||
|
||||
const SettingsSonarrView: FunctionComponent = () => {
|
||||
const baseUrlOverride = useCallback((settings: Settings) => {
|
||||
return settings.sonarr.base_url?.slice(1) ?? "";
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Layout name="Sonarr">
|
||||
<Section header="Use Sonarr">
|
||||
|
@ -36,8 +33,10 @@ const SettingsSonarrView: FunctionComponent = () => {
|
|||
label="Base URL"
|
||||
icon="/"
|
||||
settingKey="settings-sonarr-base_url"
|
||||
override={baseUrlOverride}
|
||||
beforeStaged={(v) => "/" + v}
|
||||
settingOptions={{
|
||||
onLoaded: BaseUrlModification,
|
||||
onSubmit: (v) => "/" + v,
|
||||
}}
|
||||
></Text>
|
||||
<Text label="API Key" settingKey="settings-sonarr-apikey"></Text>
|
||||
<Check label="SSL" settingKey="settings-sonarr-ssl"></Check>
|
||||
|
|
|
@ -11,6 +11,10 @@ import {
|
|||
Slider,
|
||||
Text,
|
||||
} from "../components";
|
||||
import {
|
||||
SubzeroColorModification,
|
||||
SubzeroModification,
|
||||
} from "../utilities/modifications";
|
||||
import {
|
||||
adaptiveSearchingDelayOption,
|
||||
adaptiveSearchingDeltaOption,
|
||||
|
@ -20,18 +24,6 @@ import {
|
|||
hiExtensionOptions,
|
||||
} from "./options";
|
||||
|
||||
const subzeroOverride = (key: string) => {
|
||||
return (settings: Settings) => {
|
||||
return settings.general.subzero_mods?.includes(key) ?? false;
|
||||
};
|
||||
};
|
||||
|
||||
const subzeroColorOverride = (settings: Settings) => {
|
||||
return (
|
||||
settings.general.subzero_mods?.find((v) => v.startsWith("color")) ?? ""
|
||||
);
|
||||
};
|
||||
|
||||
interface CommandOption {
|
||||
option: string;
|
||||
description: string;
|
||||
|
@ -179,7 +171,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
clearable
|
||||
placeholder="Select a provider"
|
||||
settingKey="settings-general-anti_captcha_provider"
|
||||
beforeStaged={(v) => (v === undefined ? "None" : v)}
|
||||
settingOptions={{ onSubmit: (v) => (v === undefined ? "None" : v) }}
|
||||
options={antiCaptchaOption}
|
||||
></Selector>
|
||||
<Message>Choose the anti-captcha provider you want to use</Message>
|
||||
|
@ -224,7 +216,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
<CollapseBox settingKey="settings-general-adaptive_searching">
|
||||
<Selector
|
||||
settingKey="settings-general-adaptive_searching_delay"
|
||||
beforeStaged={(v) => (v === undefined ? "3w" : v)}
|
||||
settingOptions={{ onSaved: (v) => (v === undefined ? "3w" : v) }}
|
||||
options={adaptiveSearchingDelayOption}
|
||||
></Selector>
|
||||
<Message>
|
||||
|
@ -233,7 +225,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
</Message>
|
||||
<Selector
|
||||
settingKey="settings-general-adaptive_searching_delta"
|
||||
beforeStaged={(v) => (v === undefined ? "1w" : v)}
|
||||
settingOptions={{ onSaved: (v) => (v === undefined ? "1w" : v) }}
|
||||
options={adaptiveSearchingDeltaOption}
|
||||
></Selector>
|
||||
<Message>
|
||||
|
@ -299,7 +291,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
</Message>
|
||||
<Check
|
||||
label="Hearing Impaired"
|
||||
override={subzeroOverride("remove_HI")}
|
||||
settingOptions={{ onLoaded: SubzeroModification("remove_HI") }}
|
||||
settingKey="subzero-remove_HI"
|
||||
></Check>
|
||||
<Message>
|
||||
|
@ -308,7 +300,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
</Message>
|
||||
<Check
|
||||
label="Remove Tags"
|
||||
override={subzeroOverride("remove_tags")}
|
||||
settingOptions={{ onLoaded: SubzeroModification("remove_tags") }}
|
||||
settingKey="subzero-remove_tags"
|
||||
></Check>
|
||||
<Message>
|
||||
|
@ -317,7 +309,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
</Message>
|
||||
<Check
|
||||
label="OCR Fixes"
|
||||
override={subzeroOverride("OCR_fixes")}
|
||||
settingOptions={{ onLoaded: SubzeroModification("OCR_fixes") }}
|
||||
settingKey="subzero-OCR_fixes"
|
||||
></Check>
|
||||
<Message>
|
||||
|
@ -326,7 +318,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
</Message>
|
||||
<Check
|
||||
label="Common Fixes"
|
||||
override={subzeroOverride("common")}
|
||||
settingOptions={{ onLoaded: SubzeroModification("common") }}
|
||||
settingKey="subzero-common"
|
||||
></Check>
|
||||
<Message>
|
||||
|
@ -334,7 +326,9 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
</Message>
|
||||
<Check
|
||||
label="Fix Uppercase"
|
||||
override={subzeroOverride("fix_uppercase")}
|
||||
settingOptions={{
|
||||
onLoaded: SubzeroModification("fix_uppercase"),
|
||||
}}
|
||||
settingKey="subzero-fix_uppercase"
|
||||
></Check>
|
||||
<Message>
|
||||
|
@ -345,7 +339,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
label="Color"
|
||||
clearable
|
||||
options={colorOptions}
|
||||
override={subzeroColorOverride}
|
||||
settingOptions={{ onLoaded: SubzeroColorModification }}
|
||||
settingKey="subzero-color"
|
||||
></Selector>
|
||||
<Message>
|
||||
|
@ -355,7 +349,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
|
|||
</Message>
|
||||
<Check
|
||||
label="Reverse RTL"
|
||||
override={subzeroOverride("reverse_rtl")}
|
||||
settingOptions={{ onLoaded: SubzeroModification("reverse_rtl") }}
|
||||
settingKey="subzero-reverse_rtl"
|
||||
></Check>
|
||||
<Message>
|
||||
|
|
|
@ -13,7 +13,7 @@ const SettingsUIView: FunctionComponent = () => {
|
|||
options={pageSizeOptions}
|
||||
location="storages"
|
||||
settingKey={uiPageSizeKey}
|
||||
override={(_) => pageSize}
|
||||
settingOptions={{ onLoaded: () => pageSize }}
|
||||
></Selector>
|
||||
</Section>
|
||||
</Layout>
|
||||
|
|
|
@ -8,28 +8,27 @@ import { faSave } from "@fortawesome/free-solid-svg-icons";
|
|||
import { Container, Group, LoadingOverlay } from "@mantine/core";
|
||||
import { useDocumentTitle, useForm } from "@mantine/hooks";
|
||||
import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
|
||||
import {
|
||||
enabledLanguageKey,
|
||||
languageProfileKey,
|
||||
notificationsKey,
|
||||
} from "../keys";
|
||||
import { enabledLanguageKey, languageProfileKey } from "../keys";
|
||||
import { FormContext, FormValues } from "../utilities/FormValues";
|
||||
import { SettingsProvider } from "../utilities/SettingsProvider";
|
||||
|
||||
function submitHooks(settings: LooseObject) {
|
||||
if (languageProfileKey in settings) {
|
||||
const item = settings[languageProfileKey];
|
||||
settings[languageProfileKey] = JSON.stringify(item);
|
||||
}
|
||||
type SubmitHookType = {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
[key: string]: (value: any) => unknown;
|
||||
};
|
||||
|
||||
if (enabledLanguageKey in settings) {
|
||||
const item = settings[enabledLanguageKey] as Language.Info[];
|
||||
settings[enabledLanguageKey] = item.map((v) => v.code2);
|
||||
}
|
||||
export const submitHooks: SubmitHookType = {
|
||||
[languageProfileKey]: (value) => JSON.stringify(value),
|
||||
[enabledLanguageKey]: (value: Language.Info[]) => value.map((v) => v.code2),
|
||||
};
|
||||
|
||||
if (notificationsKey in settings) {
|
||||
const item = settings[notificationsKey] as Settings.NotificationInfo[];
|
||||
settings[notificationsKey] = item.map((v) => JSON.stringify(v));
|
||||
function invokeHooks(settings: LooseObject) {
|
||||
for (const key in settings) {
|
||||
if (key in submitHooks) {
|
||||
const value = settings[key];
|
||||
const fn = submitHooks[key];
|
||||
settings[key] = fn(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -65,7 +64,7 @@ const Layout: FunctionComponent<Props> = (props) => {
|
|||
|
||||
if (Object.keys(settings).length > 0) {
|
||||
const settingsToSubmit = { ...settings };
|
||||
submitHooks(settingsToSubmit);
|
||||
invokeHooks(settingsToSubmit);
|
||||
LOG("info", "submitting settings", settingsToSubmit);
|
||||
mutate(settingsToSubmit);
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { Collapse, Stack } from "@mantine/core";
|
||||
import { FunctionComponent, useMemo, useRef } from "react";
|
||||
import { useSettingValue } from "./hooks";
|
||||
import { useSettingValue } from "../utilities/hooks";
|
||||
|
||||
interface ContentProps {
|
||||
settingKey: string;
|
||||
|
|
|
@ -22,38 +22,20 @@ import {
|
|||
TextInput,
|
||||
TextInputProps,
|
||||
} from "@mantine/core";
|
||||
import { FunctionComponent, ReactText, useCallback } from "react";
|
||||
import { useSettingValue } from ".";
|
||||
import { FormKey, useFormActions } from "../utilities/FormValues";
|
||||
import { OverrideFuncType } from "./hooks";
|
||||
|
||||
export interface BaseInput<T> {
|
||||
disabled?: boolean;
|
||||
settingKey: string;
|
||||
location?: FormKey;
|
||||
override?: OverrideFuncType<T>;
|
||||
beforeStaged?: (v: T) => unknown;
|
||||
}
|
||||
import { FunctionComponent, ReactText } from "react";
|
||||
import { BaseInput, useBaseInput } from "../utilities/hooks";
|
||||
|
||||
export type NumberProps = BaseInput<number> & NumberInputProps;
|
||||
|
||||
export const Number: FunctionComponent<NumberProps> = ({
|
||||
beforeStaged,
|
||||
override,
|
||||
settingKey,
|
||||
location,
|
||||
...props
|
||||
}) => {
|
||||
const value = useSettingValue<number>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
export const Number: FunctionComponent<NumberProps> = (props) => {
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<NumberInput
|
||||
{...props}
|
||||
{...rest}
|
||||
value={value ?? undefined}
|
||||
onChange={(val = 0) => {
|
||||
const value = beforeStaged ? beforeStaged(val) : val;
|
||||
setValue(value, settingKey, location);
|
||||
update(val);
|
||||
}}
|
||||
></NumberInput>
|
||||
);
|
||||
|
@ -61,24 +43,15 @@ export const Number: FunctionComponent<NumberProps> = ({
|
|||
|
||||
export type TextProps = BaseInput<ReactText> & TextInputProps;
|
||||
|
||||
export const Text: FunctionComponent<TextProps> = ({
|
||||
beforeStaged,
|
||||
override,
|
||||
settingKey,
|
||||
location,
|
||||
...props
|
||||
}) => {
|
||||
const value = useSettingValue<ReactText>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
export const Text: FunctionComponent<TextProps> = (props) => {
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<TextInput
|
||||
{...props}
|
||||
{...rest}
|
||||
value={value ?? undefined}
|
||||
onChange={(e) => {
|
||||
const val = e.currentTarget.value;
|
||||
const value = beforeStaged ? beforeStaged(val) : val;
|
||||
setValue(value, settingKey, location);
|
||||
update(e.currentTarget.value);
|
||||
}}
|
||||
></TextInput>
|
||||
);
|
||||
|
@ -86,24 +59,15 @@ export const Text: FunctionComponent<TextProps> = ({
|
|||
|
||||
export type PasswordProps = BaseInput<string> & PasswordInputProps;
|
||||
|
||||
export const Password: FunctionComponent<PasswordProps> = ({
|
||||
settingKey,
|
||||
location,
|
||||
override,
|
||||
beforeStaged,
|
||||
...props
|
||||
}) => {
|
||||
const value = useSettingValue<ReactText>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
export const Password: FunctionComponent<PasswordProps> = (props) => {
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<PasswordInput
|
||||
{...props}
|
||||
{...rest}
|
||||
value={value ?? undefined}
|
||||
onChange={(e) => {
|
||||
const val = e.currentTarget.value;
|
||||
const value = beforeStaged ? beforeStaged(val) : val;
|
||||
setValue(value, settingKey, location);
|
||||
update(e.currentTarget.value);
|
||||
}}
|
||||
></PasswordInput>
|
||||
);
|
||||
|
@ -116,23 +80,18 @@ export interface CheckProps extends BaseInput<boolean> {
|
|||
|
||||
export const Check: FunctionComponent<CheckProps> = ({
|
||||
label,
|
||||
override,
|
||||
disabled,
|
||||
settingKey,
|
||||
location,
|
||||
inline,
|
||||
...props
|
||||
}) => {
|
||||
const value = useSettingValue<boolean>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<Switch
|
||||
id={settingKey}
|
||||
label={label}
|
||||
onChange={(e) => {
|
||||
const { checked } = e.currentTarget;
|
||||
setValue(checked, settingKey, location);
|
||||
update(e.currentTarget.checked);
|
||||
}}
|
||||
disabled={disabled}
|
||||
disabled={rest.disabled}
|
||||
checked={value ?? false}
|
||||
></Switch>
|
||||
);
|
||||
|
@ -142,20 +101,10 @@ export type SelectorProps<T extends string | number> = BaseInput<T> &
|
|||
GlobalSelectorProps<T>;
|
||||
|
||||
export function Selector<T extends string | number>(props: SelectorProps<T>) {
|
||||
const { settingKey, location, override, beforeStaged, ...selector } = props;
|
||||
|
||||
const value = useSettingValue<T>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<GlobalSelector
|
||||
{...selector}
|
||||
value={value}
|
||||
onChange={(v) => {
|
||||
const result = beforeStaged && v ? beforeStaged(v) : v;
|
||||
setValue(result, settingKey, location);
|
||||
}}
|
||||
></GlobalSelector>
|
||||
<GlobalSelector {...rest} value={value} onChange={update}></GlobalSelector>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -165,19 +114,13 @@ export type MultiSelectorProps<T extends string | number> = BaseInput<T[]> &
|
|||
export function MultiSelector<T extends string | number>(
|
||||
props: MultiSelectorProps<T>
|
||||
) {
|
||||
const { settingKey, location, override, beforeStaged, ...selector } = props;
|
||||
|
||||
const value = useSettingValue<T[]>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<GlobalMultiSelector
|
||||
{...selector}
|
||||
{...rest}
|
||||
value={value ?? []}
|
||||
onChange={(v) => {
|
||||
const result = beforeStaged && v ? beforeStaged(v) : v;
|
||||
setValue(result, settingKey, location);
|
||||
}}
|
||||
onChange={update}
|
||||
></GlobalMultiSelector>
|
||||
);
|
||||
}
|
||||
|
@ -186,22 +129,19 @@ type SliderProps = BaseInput<number> &
|
|||
Omit<MantineSliderProps, "onChange" | "onChangeEnd" | "marks">;
|
||||
|
||||
export const Slider: FunctionComponent<SliderProps> = (props) => {
|
||||
const { settingKey, location, override, label, ...slider } = props;
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
const value = useSettingValue<number>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
const { min = 0, max = 100 } = props;
|
||||
|
||||
const marks = useSliderMarks([(slider.min = 0), (slider.max = 100)]);
|
||||
const marks = useSliderMarks([min, max]);
|
||||
|
||||
return (
|
||||
<InputWrapper label={label}>
|
||||
<InputWrapper label={rest.label}>
|
||||
<MantineSlider
|
||||
{...rest}
|
||||
marks={marks}
|
||||
onChange={(v) => {
|
||||
setValue(v, settingKey, location);
|
||||
}}
|
||||
onChange={update}
|
||||
value={value ?? 0}
|
||||
{...slider}
|
||||
></MantineSlider>
|
||||
</InputWrapper>
|
||||
);
|
||||
|
@ -211,47 +151,28 @@ type ChipsProp = BaseInput<string[]> &
|
|||
Omit<ChipInputProps, "onChange" | "data">;
|
||||
|
||||
export const Chips: FunctionComponent<ChipsProp> = (props) => {
|
||||
const { settingKey, location, override, ...chips } = props;
|
||||
|
||||
const value = useSettingValue<string[]>(settingKey, override);
|
||||
const { setValue } = useFormActions();
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<ChipInput
|
||||
value={value ?? []}
|
||||
onChange={(v) => {
|
||||
setValue(v, settingKey, location);
|
||||
}}
|
||||
{...chips}
|
||||
></ChipInput>
|
||||
<ChipInput {...rest} value={value ?? []} onChange={update}></ChipInput>
|
||||
);
|
||||
};
|
||||
|
||||
type ActionProps = {
|
||||
onClick?: (update: (v: unknown) => void, value?: string) => void;
|
||||
} & Omit<BaseInput<string>, "override" | "beforeStaged">;
|
||||
onClick?: (update: (v: string) => void, value?: string) => void;
|
||||
} & Omit<BaseInput<string>, "modification">;
|
||||
|
||||
export const Action: FunctionComponent<
|
||||
Override<ActionProps, GlobalActionProps>
|
||||
> = (props) => {
|
||||
const { onClick, settingKey, location, ...button } = props;
|
||||
|
||||
const value = useSettingValue<string>(settingKey);
|
||||
const { setValue } = useFormActions();
|
||||
|
||||
const wrappedSetValue = useCallback(
|
||||
(v: unknown) => {
|
||||
setValue(v, settingKey, location);
|
||||
},
|
||||
[location, setValue, settingKey]
|
||||
);
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<GlobalAction
|
||||
{...rest}
|
||||
onClick={() => {
|
||||
onClick?.(wrappedSetValue, value ?? undefined);
|
||||
props.onClick?.(update, (value as string) ?? undefined);
|
||||
}}
|
||||
{...button}
|
||||
></GlobalAction>
|
||||
);
|
||||
};
|
||||
|
@ -261,17 +182,13 @@ interface FileProps extends BaseInput<string> {}
|
|||
export const File: FunctionComponent<Override<FileProps, FileBrowserProps>> = (
|
||||
props
|
||||
) => {
|
||||
const { settingKey, location, override, ...file } = props;
|
||||
const value = useSettingValue<string>(settingKey);
|
||||
const { setValue } = useFormActions();
|
||||
const { value, update, rest } = useBaseInput(props);
|
||||
|
||||
return (
|
||||
<FileBrowser
|
||||
{...rest}
|
||||
defaultValue={value ?? undefined}
|
||||
onChange={(p) => {
|
||||
setValue(p, settingKey, location);
|
||||
}}
|
||||
{...file}
|
||||
onChange={update}
|
||||
></FileBrowser>
|
||||
);
|
||||
};
|
||||
|
|
|
@ -1,86 +0,0 @@
|
|||
import { get, uniqBy } from "lodash";
|
||||
import { useCallback, useMemo, useRef } from "react";
|
||||
import { useFormActions, useStagedValues } from "../utilities/FormValues";
|
||||
import { useSettings } from "../utilities/SettingsProvider";
|
||||
|
||||
export type OverrideFuncType<T> = (settings: Settings) => T;
|
||||
|
||||
export function useExtract<T>(
|
||||
key: string,
|
||||
override?: OverrideFuncType<T>
|
||||
): Readonly<Nullable<T>> {
|
||||
const settings = useSettings();
|
||||
|
||||
const overrideRef = useRef(override);
|
||||
overrideRef.current = override;
|
||||
|
||||
const extractValue = useMemo(() => {
|
||||
if (overrideRef.current) {
|
||||
return overrideRef.current(settings);
|
||||
}
|
||||
|
||||
const path = key.replaceAll("-", ".");
|
||||
|
||||
const value = get({ settings }, path, null) as Nullable<T>;
|
||||
|
||||
return value;
|
||||
}, [key, settings]);
|
||||
|
||||
return extractValue;
|
||||
}
|
||||
|
||||
export function useSettingValue<T>(
|
||||
key: string,
|
||||
override?: OverrideFuncType<T>
|
||||
): Readonly<Nullable<T>> {
|
||||
const extractValue = useExtract<T>(key, override);
|
||||
const stagedValue = useStagedValues();
|
||||
if (key in stagedValue) {
|
||||
return stagedValue[key] as T;
|
||||
} else {
|
||||
return extractValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function useLatestArray<T>(
|
||||
key: string,
|
||||
compare: keyof T,
|
||||
override?: OverrideFuncType<T[]>
|
||||
): Readonly<Nullable<T[]>> {
|
||||
const extractValue = useExtract<T[]>(key, override);
|
||||
const stagedValue = useStagedValues();
|
||||
|
||||
let staged: T[] | undefined = undefined;
|
||||
if (key in stagedValue) {
|
||||
staged = stagedValue[key];
|
||||
}
|
||||
|
||||
return useMemo(() => {
|
||||
if (staged !== undefined && extractValue) {
|
||||
return uniqBy([...staged, ...extractValue], compare);
|
||||
} else {
|
||||
return extractValue;
|
||||
}
|
||||
}, [extractValue, staged, compare]);
|
||||
}
|
||||
|
||||
export function useUpdateArray<T>(key: string, compare: keyof T) {
|
||||
const { setValue } = useFormActions();
|
||||
const stagedValue = useStagedValues();
|
||||
|
||||
const staged: T[] = useMemo(() => {
|
||||
if (key in stagedValue) {
|
||||
return stagedValue[key];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [key, stagedValue]);
|
||||
|
||||
return useCallback(
|
||||
(v: T) => {
|
||||
const newArray = uniqBy([v, ...staged], compare);
|
||||
setValue(newArray, key);
|
||||
},
|
||||
[staged, compare, setValue, key]
|
||||
);
|
||||
}
|
|
@ -1,7 +1,7 @@
|
|||
import api from "@/apis/raw";
|
||||
import { Button } from "@mantine/core";
|
||||
import { FunctionComponent, useCallback, useState } from "react";
|
||||
import { useSettingValue } from "./hooks";
|
||||
import { useSettingValue } from "../utilities/hooks";
|
||||
|
||||
export const URLTestButton: FunctionComponent<{
|
||||
category: "sonarr" | "radarr";
|
||||
|
@ -60,7 +60,6 @@ export * from "./Card";
|
|||
export * from "./collapse";
|
||||
export { default as CollapseBox } from "./collapse";
|
||||
export * from "./forms";
|
||||
export * from "./hooks";
|
||||
export * from "./Layout";
|
||||
export { default as Layout } from "./Layout";
|
||||
export * from "./Message";
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
seriesEnabledKey,
|
||||
} from "../keys";
|
||||
import { useFormActions } from "../utilities/FormValues";
|
||||
import { useExtract, useSettingValue } from "./hooks";
|
||||
import { useSettingValue } from "../utilities/hooks";
|
||||
import { Message } from "./Message";
|
||||
|
||||
type SupportType = "sonarr" | "radarr";
|
||||
|
@ -48,7 +48,8 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
|
|||
const items = useSettingValue<[string, string][]>(key);
|
||||
|
||||
const enabledKey = getEnabledKey(type);
|
||||
const enabled = useExtract<boolean>(enabledKey);
|
||||
const enabled = useSettingValue<boolean>(enabledKey, { original: true });
|
||||
|
||||
const { setValue } = useFormActions();
|
||||
|
||||
const updateRow = useCallback(
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { LOG } from "@/utilities/console";
|
||||
import { UseForm } from "@mantine/hooks/lib/use-form/use-form";
|
||||
import { createContext, useCallback, useContext, useRef } from "react";
|
||||
|
||||
|
@ -26,6 +27,7 @@ export function useFormActions() {
|
|||
|
||||
const update = useCallback(
|
||||
(object: LooseObject, location: FormKey = "settings") => {
|
||||
LOG("info", `Updating values in ${location}`, object);
|
||||
formRef.current.setValues((values) => {
|
||||
const changes = { ...values[location], ...object };
|
||||
return { ...values, [location]: changes };
|
||||
|
@ -36,6 +38,7 @@ export function useFormActions() {
|
|||
|
||||
const setValue = useCallback(
|
||||
(v: unknown, key: string, location: FormKey = "settings") => {
|
||||
LOG("info", `Updating value of ${key} in ${location}`, v);
|
||||
formRef.current.setValues((values) => {
|
||||
const changes = { ...values[location], [key]: v };
|
||||
return { ...values, [location]: changes };
|
||||
|
|
123
frontend/src/pages/Settings/utilities/hooks.ts
Normal file
123
frontend/src/pages/Settings/utilities/hooks.ts
Normal file
|
@ -0,0 +1,123 @@
|
|||
import { LOG } from "@/utilities/console";
|
||||
import { get, isNull, isUndefined, uniqBy } from "lodash";
|
||||
import { useCallback, useEffect, useMemo, useRef } from "react";
|
||||
import { submitHooks } from "../components";
|
||||
import {
|
||||
FormKey,
|
||||
useFormActions,
|
||||
useStagedValues,
|
||||
} from "../utilities/FormValues";
|
||||
import { useSettings } from "../utilities/SettingsProvider";
|
||||
|
||||
export interface BaseInput<T> {
|
||||
disabled?: boolean;
|
||||
settingKey: string;
|
||||
location?: FormKey;
|
||||
settingOptions?: SettingValueOptions<T>;
|
||||
}
|
||||
|
||||
export type SettingValueOptions<T> = {
|
||||
original?: boolean;
|
||||
defaultValue?: T;
|
||||
onLoaded?: (settings: Settings) => T;
|
||||
onSaved?: (value: T) => unknown;
|
||||
onSubmit?: (value: T) => unknown;
|
||||
};
|
||||
|
||||
export function useBaseInput<T, V>(props: T & BaseInput<V>) {
|
||||
const { settingKey, settingOptions, location, ...rest } = props;
|
||||
// TODO: Opti options
|
||||
const value = useSettingValue<V>(settingKey, settingOptions);
|
||||
|
||||
const { setValue } = useFormActions();
|
||||
|
||||
const update = useCallback(
|
||||
(newValue: V | null) => {
|
||||
const moddedValue =
|
||||
(newValue && settingOptions?.onSaved?.(newValue)) ?? newValue;
|
||||
|
||||
setValue(moddedValue, settingKey, location);
|
||||
},
|
||||
[settingOptions, setValue, settingKey, location]
|
||||
);
|
||||
|
||||
return { value, update, rest };
|
||||
}
|
||||
|
||||
export function useSettingValue<T>(
|
||||
key: string,
|
||||
options?: SettingValueOptions<T>
|
||||
): Readonly<Nullable<T>> {
|
||||
const settings = useSettings();
|
||||
|
||||
const optionsRef = useRef(options);
|
||||
|
||||
useEffect(() => {
|
||||
const onSubmit = optionsRef.current?.onSubmit;
|
||||
if (onSubmit) {
|
||||
LOG("info", "Adding submit hook for", key);
|
||||
submitHooks[key] = onSubmit;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (key in submitHooks) {
|
||||
LOG("info", "Removing submit hook for", key);
|
||||
delete submitHooks[key];
|
||||
}
|
||||
};
|
||||
}, [key]);
|
||||
|
||||
const originalValue = useMemo(() => {
|
||||
const onLoaded = optionsRef.current?.onLoaded;
|
||||
const defaultValue = optionsRef.current?.defaultValue;
|
||||
if (onLoaded) {
|
||||
LOG("info", `${key} is using custom loader`);
|
||||
|
||||
return onLoaded(settings);
|
||||
}
|
||||
|
||||
const path = key.replaceAll("-", ".");
|
||||
|
||||
const value = get({ settings }, path, null) as Nullable<T>;
|
||||
|
||||
if (defaultValue && (isNull(value) || isUndefined(value))) {
|
||||
LOG("info", `${key} is falling back to`, defaultValue);
|
||||
|
||||
return defaultValue;
|
||||
}
|
||||
|
||||
return value;
|
||||
}, [key, settings]);
|
||||
|
||||
const stagedValue = useStagedValues();
|
||||
|
||||
if (key in stagedValue && optionsRef.current?.original !== true) {
|
||||
return stagedValue[key] as T;
|
||||
} else {
|
||||
return originalValue;
|
||||
}
|
||||
}
|
||||
|
||||
export function useUpdateArray<T>(key: string, compare: keyof T) {
|
||||
const { setValue } = useFormActions();
|
||||
const stagedValue = useStagedValues();
|
||||
|
||||
const compareRef = useRef(compare);
|
||||
compareRef.current = compare;
|
||||
|
||||
const staged: T[] = useMemo(() => {
|
||||
if (key in stagedValue) {
|
||||
return stagedValue[key];
|
||||
} else {
|
||||
return [];
|
||||
}
|
||||
}, [key, stagedValue]);
|
||||
|
||||
return useCallback(
|
||||
(v: T) => {
|
||||
const newArray = uniqBy([v, ...staged], compareRef.current);
|
||||
setValue(newArray, key);
|
||||
},
|
||||
[staged, setValue, key]
|
||||
);
|
||||
}
|
8
frontend/src/pages/Settings/utilities/modifications.ts
Normal file
8
frontend/src/pages/Settings/utilities/modifications.ts
Normal file
|
@ -0,0 +1,8 @@
|
|||
export const BaseUrlModification = (settings: Settings) =>
|
||||
settings.general.base_url?.slice(1) ?? "";
|
||||
|
||||
export const SubzeroModification = (key: string) => (settings: Settings) =>
|
||||
settings.general.subzero_mods?.includes(key) ?? false;
|
||||
|
||||
export const SubzeroColorModification = (settings: Settings) =>
|
||||
settings.general.subzero_mods?.find((v) => v.startsWith("color")) ?? "";
|
|
@ -26,7 +26,7 @@ const UIError: FunctionComponent<Props> = ({ error }) => {
|
|||
let callStack = error.stack ?? "";
|
||||
|
||||
// Remove sensitive information from the stack
|
||||
callStack = callStack.replaceAll(window.location.hostname, Placeholder);
|
||||
callStack = callStack.replaceAll(window.location.host, Placeholder);
|
||||
|
||||
return callStack;
|
||||
}, [error.stack]);
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue