Fix #1872, refactor the settings builder

pull/1888/head
LASER-Yi 3 years ago
parent d2b40bd781
commit d9c334d43a

@ -21,6 +21,7 @@ import {
Selector, Selector,
Text, Text,
} from "../components"; } from "../components";
import { BaseUrlModification } from "../utilities/modifications";
import { branchOptions, proxyOptions, securityOptions } from "./options"; import { branchOptions, proxyOptions, securityOptions } from "./options";
const characters = "abcdef0123456789"; const characters = "abcdef0123456789";
@ -33,9 +34,6 @@ const generateApiKey = () => {
.join(""); .join("");
}; };
const baseUrlOverride = (settings: Settings) =>
settings.general.base_url?.slice(1) ?? "";
const SettingsGeneralView: FunctionComponent = () => { const SettingsGeneralView: FunctionComponent = () => {
const [copied, setCopy] = useState(false); const [copied, setCopy] = useState(false);
@ -59,8 +57,10 @@ const SettingsGeneralView: FunctionComponent = () => {
label="Base URL" label="Base URL"
icon="/" icon="/"
settingKey="settings-general-base_url" settingKey="settings-general-base_url"
override={baseUrlOverride} settingOptions={{
beforeStaged={(v) => "/" + v} onLoaded: BaseUrlModification,
onSubmit: (v) => "/" + v,
}}
></Text> ></Text>
<Message>Reverse proxy support</Message> <Message>Reverse proxy support</Message>
</Section> </Section>
@ -71,7 +71,9 @@ const SettingsGeneralView: FunctionComponent = () => {
options={securityOptions} options={securityOptions}
placeholder="No Authentication" placeholder="No Authentication"
settingKey="settings-auth-type" settingKey="settings-auth-type"
beforeStaged={(v) => (v === null ? "None" : v)} settingOptions={{
onSubmit: (v) => (v === null ? "None" : v),
}}
></Selector> ></Selector>
<CollapseBox settingKey="settings-auth-type"> <CollapseBox settingKey="settings-auth-type">
<Text label="Username" settingKey="settings-auth-username"></Text> <Text label="Username" settingKey="settings-auth-username"></Text>
@ -121,7 +123,9 @@ const SettingsGeneralView: FunctionComponent = () => {
settingKey="settings-proxy-type" settingKey="settings-proxy-type"
placeholder="No Proxy" placeholder="No Proxy"
options={proxyOptions} options={proxyOptions}
beforeStaged={(v) => (v === null ? "None" : v)} settingOptions={{
onSubmit: (v) => (v === null ? "None" : v),
}}
></Selector> ></Selector>
<CollapseBox <CollapseBox
settingKey="settings-proxy-type" settingKey="settings-proxy-type"

@ -8,8 +8,9 @@ import { useSelectorOptions } from "@/utilities";
import { InputWrapper } from "@mantine/core"; import { InputWrapper } from "@mantine/core";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { useLatestEnabledLanguages, useLatestProfiles } from "."; import { useLatestEnabledLanguages, useLatestProfiles } from ".";
import { BaseInput, Selector, SelectorProps } from "../components"; import { Selector, SelectorProps } from "../components";
import { useFormActions } from "../utilities/FormValues"; import { useFormActions } from "../utilities/FormValues";
import { BaseInput } from "../utilities/hooks";
type LanguageSelectorProps = Omit< type LanguageSelectorProps = Omit<
MultiSelectorProps<Language.Info>, MultiSelectorProps<Language.Info>,
@ -41,7 +42,7 @@ export const LanguageSelector: FunctionComponent<
}; };
export const ProfileSelector: FunctionComponent< export const ProfileSelector: FunctionComponent<
Omit<SelectorProps<number>, "beforeStaged" | "options" | "clearable"> Omit<SelectorProps<number>, "settingOptions" | "options" | "clearable">
> = ({ ...props }) => { > = ({ ...props }) => {
const profiles = useLatestProfiles(); const profiles = useLatestProfiles();
@ -58,7 +59,7 @@ export const ProfileSelector: FunctionComponent<
{...props} {...props}
clearable clearable
options={profileOptions} options={profileOptions}
beforeStaged={(v) => (v === null ? "" : v)} settingOptions={{ onSubmit: (v) => (v === null ? "" : v) }}
></Selector> ></Selector>
); );
}; };

@ -1,15 +1,9 @@
import { useLanguageProfiles, useLanguages } from "@/apis/hooks"; import { useLanguageProfiles, useLanguages } from "@/apis/hooks";
import { useEnabledLanguages } from "@/utilities/languages"; import { useEnabledLanguages } from "@/utilities/languages";
import { FunctionComponent } from "react"; import { FunctionComponent } from "react";
import { import { Check, CollapseBox, Layout, Message, Section } from "../components";
Check,
CollapseBox,
Layout,
Message,
Section,
useSettingValue,
} from "../components";
import { enabledLanguageKey, languageProfileKey } from "../keys"; import { enabledLanguageKey, languageProfileKey } from "../keys";
import { useSettingValue } from "../utilities/hooks";
import { LanguageSelector, ProfileSelector } from "./components"; import { LanguageSelector, ProfileSelector } from "./components";
import Table from "./table"; import Table from "./table";

@ -14,8 +14,9 @@ import {
import { useForm } from "@mantine/hooks"; import { useForm } from "@mantine/hooks";
import { FunctionComponent, useMemo } from "react"; import { FunctionComponent, useMemo } from "react";
import { useMutation } from "react-query"; import { useMutation } from "react-query";
import { Card, useLatestArray, useUpdateArray } from "../components"; import { Card } from "../components";
import { notificationsKey } from "../keys"; import { notificationsKey } from "../keys";
import { useSettingValue, useUpdateArray } from "../utilities/hooks";
interface Props { interface Props {
selections: readonly Settings.NotificationInfo[]; selections: readonly Settings.NotificationInfo[];
@ -100,10 +101,12 @@ const NotificationModal = withModal(NotificationForm, "notification-tool", {
}); });
export const NotificationView: FunctionComponent = () => { export const NotificationView: FunctionComponent = () => {
const notifications = useLatestArray<Settings.NotificationInfo>( const notifications = useSettingValue<Settings.NotificationInfo[]>(
notificationsKey, notificationsKey,
"name", {
(s) => s.notifications.providers onLoaded: (settings) => settings.notifications.providers,
onSubmit: (value) => value.map((v) => JSON.stringify(v)),
}
); );
const update = useUpdateArray<Settings.NotificationInfo>( const update = useUpdateArray<Settings.NotificationInfo>(

@ -20,21 +20,14 @@ import {
useRef, useRef,
useState, useState,
} from "react"; } from "react";
import { import { Card, Check, Chips, Message, Password, Text } from "../components";
Card,
Check,
Chips,
Message,
Password,
Text,
useSettingValue,
} from "../components";
import { import {
FormContext, FormContext,
FormValues, FormValues,
useFormActions, useFormActions,
useStagedValues, useStagedValues,
} from "../utilities/FormValues"; } from "../utilities/FormValues";
import { useSettingValue } from "../utilities/hooks";
import { SettingsProvider, useSettings } from "../utilities/SettingsProvider"; import { SettingsProvider, useSettings } from "../utilities/SettingsProvider";
import { ProviderInfo, ProviderList } from "./list"; import { ProviderInfo, ProviderList } from "./list";

@ -1,5 +1,5 @@
import { Code } from "@mantine/core"; import { Code } from "@mantine/core";
import { FunctionComponent, useCallback } from "react"; import { FunctionComponent } from "react";
import { import {
Check, Check,
Chips, Chips,
@ -14,12 +14,9 @@ import {
URLTestButton, URLTestButton,
} from "../components"; } from "../components";
import { moviesEnabledKey } from "../keys"; import { moviesEnabledKey } from "../keys";
import { BaseUrlModification } from "../utilities/modifications";
const SettingsRadarrView: FunctionComponent = () => { const SettingsRadarrView: FunctionComponent = () => {
const baseUrlOverride = useCallback((settings: Settings) => {
return settings.radarr.base_url?.slice(1) ?? "";
}, []);
return ( return (
<Layout name="Radarr"> <Layout name="Radarr">
<Section header="Use Radarr"> <Section header="Use Radarr">
@ -34,8 +31,10 @@ const SettingsRadarrView: FunctionComponent = () => {
label="Base URL" label="Base URL"
icon="/" icon="/"
settingKey="settings-radarr-base_url" settingKey="settings-radarr-base_url"
override={baseUrlOverride} settingOptions={{
beforeStaged={(v) => "/" + v} onLoaded: BaseUrlModification,
onSubmit: (v) => "/" + v,
}}
></Text> ></Text>
<Text label="API Key" settingKey="settings-radarr-apikey"></Text> <Text label="API Key" settingKey="settings-radarr-apikey"></Text>
<Check label="SSL" settingKey="settings-radarr-ssl"></Check> <Check label="SSL" settingKey="settings-radarr-ssl"></Check>

@ -1,5 +1,5 @@
import { Code } from "@mantine/core"; import { Code } from "@mantine/core";
import { FunctionComponent, useCallback } from "react"; import { FunctionComponent } from "react";
import { import {
Check, Check,
Chips, Chips,
@ -16,12 +16,9 @@ import {
} from "../components"; } from "../components";
import { seriesEnabledKey } from "../keys"; import { seriesEnabledKey } from "../keys";
import { seriesTypeOptions } from "../options"; import { seriesTypeOptions } from "../options";
import { BaseUrlModification } from "../utilities/modifications";
const SettingsSonarrView: FunctionComponent = () => { const SettingsSonarrView: FunctionComponent = () => {
const baseUrlOverride = useCallback((settings: Settings) => {
return settings.sonarr.base_url?.slice(1) ?? "";
}, []);
return ( return (
<Layout name="Sonarr"> <Layout name="Sonarr">
<Section header="Use Sonarr"> <Section header="Use Sonarr">
@ -36,8 +33,10 @@ const SettingsSonarrView: FunctionComponent = () => {
label="Base URL" label="Base URL"
icon="/" icon="/"
settingKey="settings-sonarr-base_url" settingKey="settings-sonarr-base_url"
override={baseUrlOverride} settingOptions={{
beforeStaged={(v) => "/" + v} onLoaded: BaseUrlModification,
onSubmit: (v) => "/" + v,
}}
></Text> ></Text>
<Text label="API Key" settingKey="settings-sonarr-apikey"></Text> <Text label="API Key" settingKey="settings-sonarr-apikey"></Text>
<Check label="SSL" settingKey="settings-sonarr-ssl"></Check> <Check label="SSL" settingKey="settings-sonarr-ssl"></Check>

@ -11,6 +11,10 @@ import {
Slider, Slider,
Text, Text,
} from "../components"; } from "../components";
import {
SubzeroColorModification,
SubzeroModification,
} from "../utilities/modifications";
import { import {
adaptiveSearchingDelayOption, adaptiveSearchingDelayOption,
adaptiveSearchingDeltaOption, adaptiveSearchingDeltaOption,
@ -20,18 +24,6 @@ import {
hiExtensionOptions, hiExtensionOptions,
} from "./options"; } 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 { interface CommandOption {
option: string; option: string;
description: string; description: string;
@ -179,7 +171,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
clearable clearable
placeholder="Select a provider" placeholder="Select a provider"
settingKey="settings-general-anti_captcha_provider" settingKey="settings-general-anti_captcha_provider"
beforeStaged={(v) => (v === undefined ? "None" : v)} settingOptions={{ onSubmit: (v) => (v === undefined ? "None" : v) }}
options={antiCaptchaOption} options={antiCaptchaOption}
></Selector> ></Selector>
<Message>Choose the anti-captcha provider you want to use</Message> <Message>Choose the anti-captcha provider you want to use</Message>
@ -224,7 +216,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
<CollapseBox settingKey="settings-general-adaptive_searching"> <CollapseBox settingKey="settings-general-adaptive_searching">
<Selector <Selector
settingKey="settings-general-adaptive_searching_delay" settingKey="settings-general-adaptive_searching_delay"
beforeStaged={(v) => (v === undefined ? "3w" : v)} settingOptions={{ onSaved: (v) => (v === undefined ? "3w" : v) }}
options={adaptiveSearchingDelayOption} options={adaptiveSearchingDelayOption}
></Selector> ></Selector>
<Message> <Message>
@ -233,7 +225,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message> </Message>
<Selector <Selector
settingKey="settings-general-adaptive_searching_delta" settingKey="settings-general-adaptive_searching_delta"
beforeStaged={(v) => (v === undefined ? "1w" : v)} settingOptions={{ onSaved: (v) => (v === undefined ? "1w" : v) }}
options={adaptiveSearchingDeltaOption} options={adaptiveSearchingDeltaOption}
></Selector> ></Selector>
<Message> <Message>
@ -299,7 +291,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message> </Message>
<Check <Check
label="Hearing Impaired" label="Hearing Impaired"
override={subzeroOverride("remove_HI")} settingOptions={{ onLoaded: SubzeroModification("remove_HI") }}
settingKey="subzero-remove_HI" settingKey="subzero-remove_HI"
></Check> ></Check>
<Message> <Message>
@ -308,7 +300,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message> </Message>
<Check <Check
label="Remove Tags" label="Remove Tags"
override={subzeroOverride("remove_tags")} settingOptions={{ onLoaded: SubzeroModification("remove_tags") }}
settingKey="subzero-remove_tags" settingKey="subzero-remove_tags"
></Check> ></Check>
<Message> <Message>
@ -317,7 +309,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message> </Message>
<Check <Check
label="OCR Fixes" label="OCR Fixes"
override={subzeroOverride("OCR_fixes")} settingOptions={{ onLoaded: SubzeroModification("OCR_fixes") }}
settingKey="subzero-OCR_fixes" settingKey="subzero-OCR_fixes"
></Check> ></Check>
<Message> <Message>
@ -326,7 +318,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message> </Message>
<Check <Check
label="Common Fixes" label="Common Fixes"
override={subzeroOverride("common")} settingOptions={{ onLoaded: SubzeroModification("common") }}
settingKey="subzero-common" settingKey="subzero-common"
></Check> ></Check>
<Message> <Message>
@ -334,7 +326,9 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message> </Message>
<Check <Check
label="Fix Uppercase" label="Fix Uppercase"
override={subzeroOverride("fix_uppercase")} settingOptions={{
onLoaded: SubzeroModification("fix_uppercase"),
}}
settingKey="subzero-fix_uppercase" settingKey="subzero-fix_uppercase"
></Check> ></Check>
<Message> <Message>
@ -345,7 +339,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
label="Color" label="Color"
clearable clearable
options={colorOptions} options={colorOptions}
override={subzeroColorOverride} settingOptions={{ onLoaded: SubzeroColorModification }}
settingKey="subzero-color" settingKey="subzero-color"
></Selector> ></Selector>
<Message> <Message>
@ -355,7 +349,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
</Message> </Message>
<Check <Check
label="Reverse RTL" label="Reverse RTL"
override={subzeroOverride("reverse_rtl")} settingOptions={{ onLoaded: SubzeroModification("reverse_rtl") }}
settingKey="subzero-reverse_rtl" settingKey="subzero-reverse_rtl"
></Check> ></Check>
<Message> <Message>

@ -13,7 +13,7 @@ const SettingsUIView: FunctionComponent = () => {
options={pageSizeOptions} options={pageSizeOptions}
location="storages" location="storages"
settingKey={uiPageSizeKey} settingKey={uiPageSizeKey}
override={(_) => pageSize} settingOptions={{ onLoaded: () => pageSize }}
></Selector> ></Selector>
</Section> </Section>
</Layout> </Layout>

@ -8,28 +8,27 @@ import { faSave } from "@fortawesome/free-solid-svg-icons";
import { Container, Group, LoadingOverlay } from "@mantine/core"; import { Container, Group, LoadingOverlay } from "@mantine/core";
import { useDocumentTitle, useForm } from "@mantine/hooks"; import { useDocumentTitle, useForm } from "@mantine/hooks";
import { FunctionComponent, ReactNode, useCallback, useMemo } from "react"; import { FunctionComponent, ReactNode, useCallback, useMemo } from "react";
import { import { enabledLanguageKey, languageProfileKey } from "../keys";
enabledLanguageKey,
languageProfileKey,
notificationsKey,
} from "../keys";
import { FormContext, FormValues } from "../utilities/FormValues"; import { FormContext, FormValues } from "../utilities/FormValues";
import { SettingsProvider } from "../utilities/SettingsProvider"; import { SettingsProvider } from "../utilities/SettingsProvider";
function submitHooks(settings: LooseObject) { type SubmitHookType = {
if (languageProfileKey in settings) { // eslint-disable-next-line @typescript-eslint/no-explicit-any
const item = settings[languageProfileKey]; [key: string]: (value: any) => unknown;
settings[languageProfileKey] = JSON.stringify(item); };
}
if (enabledLanguageKey in settings) { export const submitHooks: SubmitHookType = {
const item = settings[enabledLanguageKey] as Language.Info[]; [languageProfileKey]: (value) => JSON.stringify(value),
settings[enabledLanguageKey] = item.map((v) => v.code2); [enabledLanguageKey]: (value: Language.Info[]) => value.map((v) => v.code2),
} };
if (notificationsKey in settings) { function invokeHooks(settings: LooseObject) {
const item = settings[notificationsKey] as Settings.NotificationInfo[]; for (const key in settings) {
settings[notificationsKey] = item.map((v) => JSON.stringify(v)); 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) { if (Object.keys(settings).length > 0) {
const settingsToSubmit = { ...settings }; const settingsToSubmit = { ...settings };
submitHooks(settingsToSubmit); invokeHooks(settingsToSubmit);
LOG("info", "submitting settings", settingsToSubmit); LOG("info", "submitting settings", settingsToSubmit);
mutate(settingsToSubmit); mutate(settingsToSubmit);
} }

@ -1,6 +1,6 @@
import { Collapse, Stack } from "@mantine/core"; import { Collapse, Stack } from "@mantine/core";
import { FunctionComponent, useMemo, useRef } from "react"; import { FunctionComponent, useMemo, useRef } from "react";
import { useSettingValue } from "./hooks"; import { useSettingValue } from "../utilities/hooks";
interface ContentProps { interface ContentProps {
settingKey: string; settingKey: string;

@ -22,38 +22,20 @@ import {
TextInput, TextInput,
TextInputProps, TextInputProps,
} from "@mantine/core"; } from "@mantine/core";
import { FunctionComponent, ReactText, useCallback } from "react"; import { FunctionComponent, ReactText } from "react";
import { useSettingValue } from "."; import { BaseInput, useBaseInput } from "../utilities/hooks";
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;
}
export type NumberProps = BaseInput<number> & NumberInputProps; export type NumberProps = BaseInput<number> & NumberInputProps;
export const Number: FunctionComponent<NumberProps> = ({ export const Number: FunctionComponent<NumberProps> = (props) => {
beforeStaged, const { value, update, rest } = useBaseInput(props);
override,
settingKey,
location,
...props
}) => {
const value = useSettingValue<number>(settingKey, override);
const { setValue } = useFormActions();
return ( return (
<NumberInput <NumberInput
{...props} {...rest}
value={value ?? undefined} value={value ?? undefined}
onChange={(val = 0) => { onChange={(val = 0) => {
const value = beforeStaged ? beforeStaged(val) : val; update(val);
setValue(value, settingKey, location);
}} }}
></NumberInput> ></NumberInput>
); );
@ -61,24 +43,15 @@ export const Number: FunctionComponent<NumberProps> = ({
export type TextProps = BaseInput<ReactText> & TextInputProps; export type TextProps = BaseInput<ReactText> & TextInputProps;
export const Text: FunctionComponent<TextProps> = ({ export const Text: FunctionComponent<TextProps> = (props) => {
beforeStaged, const { value, update, rest } = useBaseInput(props);
override,
settingKey,
location,
...props
}) => {
const value = useSettingValue<ReactText>(settingKey, override);
const { setValue } = useFormActions();
return ( return (
<TextInput <TextInput
{...props} {...rest}
value={value ?? undefined} value={value ?? undefined}
onChange={(e) => { onChange={(e) => {
const val = e.currentTarget.value; update(e.currentTarget.value);
const value = beforeStaged ? beforeStaged(val) : val;
setValue(value, settingKey, location);
}} }}
></TextInput> ></TextInput>
); );
@ -86,24 +59,15 @@ export const Text: FunctionComponent<TextProps> = ({
export type PasswordProps = BaseInput<string> & PasswordInputProps; export type PasswordProps = BaseInput<string> & PasswordInputProps;
export const Password: FunctionComponent<PasswordProps> = ({ export const Password: FunctionComponent<PasswordProps> = (props) => {
settingKey, const { value, update, rest } = useBaseInput(props);
location,
override,
beforeStaged,
...props
}) => {
const value = useSettingValue<ReactText>(settingKey, override);
const { setValue } = useFormActions();
return ( return (
<PasswordInput <PasswordInput
{...props} {...rest}
value={value ?? undefined} value={value ?? undefined}
onChange={(e) => { onChange={(e) => {
const val = e.currentTarget.value; update(e.currentTarget.value);
const value = beforeStaged ? beforeStaged(val) : val;
setValue(value, settingKey, location);
}} }}
></PasswordInput> ></PasswordInput>
); );
@ -116,23 +80,18 @@ export interface CheckProps extends BaseInput<boolean> {
export const Check: FunctionComponent<CheckProps> = ({ export const Check: FunctionComponent<CheckProps> = ({
label, label,
override, inline,
disabled, ...props
settingKey,
location,
}) => { }) => {
const value = useSettingValue<boolean>(settingKey, override); const { value, update, rest } = useBaseInput(props);
const { setValue } = useFormActions();
return ( return (
<Switch <Switch
id={settingKey}
label={label} label={label}
onChange={(e) => { onChange={(e) => {
const { checked } = e.currentTarget; update(e.currentTarget.checked);
setValue(checked, settingKey, location);
}} }}
disabled={disabled} disabled={rest.disabled}
checked={value ?? false} checked={value ?? false}
></Switch> ></Switch>
); );
@ -142,20 +101,10 @@ export type SelectorProps<T extends string | number> = BaseInput<T> &
GlobalSelectorProps<T>; GlobalSelectorProps<T>;
export function Selector<T extends string | number>(props: SelectorProps<T>) { export function Selector<T extends string | number>(props: SelectorProps<T>) {
const { settingKey, location, override, beforeStaged, ...selector } = props; const { value, update, rest } = useBaseInput(props);
const value = useSettingValue<T>(settingKey, override);
const { setValue } = useFormActions();
return ( return (
<GlobalSelector <GlobalSelector {...rest} value={value} onChange={update}></GlobalSelector>
{...selector}
value={value}
onChange={(v) => {
const result = beforeStaged && v ? beforeStaged(v) : v;
setValue(result, settingKey, location);
}}
></GlobalSelector>
); );
} }
@ -165,19 +114,13 @@ export type MultiSelectorProps<T extends string | number> = BaseInput<T[]> &
export function MultiSelector<T extends string | number>( export function MultiSelector<T extends string | number>(
props: MultiSelectorProps<T> props: MultiSelectorProps<T>
) { ) {
const { settingKey, location, override, beforeStaged, ...selector } = props; const { value, update, rest } = useBaseInput(props);
const value = useSettingValue<T[]>(settingKey, override);
const { setValue } = useFormActions();
return ( return (
<GlobalMultiSelector <GlobalMultiSelector
{...selector} {...rest}
value={value ?? []} value={value ?? []}
onChange={(v) => { onChange={update}
const result = beforeStaged && v ? beforeStaged(v) : v;
setValue(result, settingKey, location);
}}
></GlobalMultiSelector> ></GlobalMultiSelector>
); );
} }
@ -186,22 +129,19 @@ type SliderProps = BaseInput<number> &
Omit<MantineSliderProps, "onChange" | "onChangeEnd" | "marks">; Omit<MantineSliderProps, "onChange" | "onChangeEnd" | "marks">;
export const Slider: FunctionComponent<SliderProps> = (props) => { 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 { min = 0, max = 100 } = props;
const { setValue } = useFormActions();
const marks = useSliderMarks([(slider.min = 0), (slider.max = 100)]); const marks = useSliderMarks([min, max]);
return ( return (
<InputWrapper label={label}> <InputWrapper label={rest.label}>
<MantineSlider <MantineSlider
{...rest}
marks={marks} marks={marks}
onChange={(v) => { onChange={update}
setValue(v, settingKey, location);
}}
value={value ?? 0} value={value ?? 0}
{...slider}
></MantineSlider> ></MantineSlider>
</InputWrapper> </InputWrapper>
); );
@ -211,47 +151,28 @@ type ChipsProp = BaseInput<string[]> &
Omit<ChipInputProps, "onChange" | "data">; Omit<ChipInputProps, "onChange" | "data">;
export const Chips: FunctionComponent<ChipsProp> = (props) => { export const Chips: FunctionComponent<ChipsProp> = (props) => {
const { settingKey, location, override, ...chips } = props; const { value, update, rest } = useBaseInput(props);
const value = useSettingValue<string[]>(settingKey, override);
const { setValue } = useFormActions();
return ( return (
<ChipInput <ChipInput {...rest} value={value ?? []} onChange={update}></ChipInput>
value={value ?? []}
onChange={(v) => {
setValue(v, settingKey, location);
}}
{...chips}
></ChipInput>
); );
}; };
type ActionProps = { type ActionProps = {
onClick?: (update: (v: unknown) => void, value?: string) => void; onClick?: (update: (v: string) => void, value?: string) => void;
} & Omit<BaseInput<string>, "override" | "beforeStaged">; } & Omit<BaseInput<string>, "modification">;
export const Action: FunctionComponent< export const Action: FunctionComponent<
Override<ActionProps, GlobalActionProps> Override<ActionProps, GlobalActionProps>
> = (props) => { > = (props) => {
const { onClick, settingKey, location, ...button } = props; const { value, update, rest } = useBaseInput(props);
const value = useSettingValue<string>(settingKey);
const { setValue } = useFormActions();
const wrappedSetValue = useCallback(
(v: unknown) => {
setValue(v, settingKey, location);
},
[location, setValue, settingKey]
);
return ( return (
<GlobalAction <GlobalAction
{...rest}
onClick={() => { onClick={() => {
onClick?.(wrappedSetValue, value ?? undefined); props.onClick?.(update, (value as string) ?? undefined);
}} }}
{...button}
></GlobalAction> ></GlobalAction>
); );
}; };
@ -261,17 +182,13 @@ interface FileProps extends BaseInput<string> {}
export const File: FunctionComponent<Override<FileProps, FileBrowserProps>> = ( export const File: FunctionComponent<Override<FileProps, FileBrowserProps>> = (
props props
) => { ) => {
const { settingKey, location, override, ...file } = props; const { value, update, rest } = useBaseInput(props);
const value = useSettingValue<string>(settingKey);
const { setValue } = useFormActions();
return ( return (
<FileBrowser <FileBrowser
{...rest}
defaultValue={value ?? undefined} defaultValue={value ?? undefined}
onChange={(p) => { onChange={update}
setValue(p, settingKey, location);
}}
{...file}
></FileBrowser> ></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 api from "@/apis/raw";
import { Button } from "@mantine/core"; import { Button } from "@mantine/core";
import { FunctionComponent, useCallback, useState } from "react"; import { FunctionComponent, useCallback, useState } from "react";
import { useSettingValue } from "./hooks"; import { useSettingValue } from "../utilities/hooks";
export const URLTestButton: FunctionComponent<{ export const URLTestButton: FunctionComponent<{
category: "sonarr" | "radarr"; category: "sonarr" | "radarr";
@ -60,7 +60,6 @@ export * from "./Card";
export * from "./collapse"; export * from "./collapse";
export { default as CollapseBox } from "./collapse"; export { default as CollapseBox } from "./collapse";
export * from "./forms"; export * from "./forms";
export * from "./hooks";
export * from "./Layout"; export * from "./Layout";
export { default as Layout } from "./Layout"; export { default as Layout } from "./Layout";
export * from "./Message"; export * from "./Message";

@ -13,7 +13,7 @@ import {
seriesEnabledKey, seriesEnabledKey,
} from "../keys"; } from "../keys";
import { useFormActions } from "../utilities/FormValues"; import { useFormActions } from "../utilities/FormValues";
import { useExtract, useSettingValue } from "./hooks"; import { useSettingValue } from "../utilities/hooks";
import { Message } from "./Message"; import { Message } from "./Message";
type SupportType = "sonarr" | "radarr"; type SupportType = "sonarr" | "radarr";
@ -48,7 +48,8 @@ export const PathMappingTable: FunctionComponent<TableProps> = ({ type }) => {
const items = useSettingValue<[string, string][]>(key); const items = useSettingValue<[string, string][]>(key);
const enabledKey = getEnabledKey(type); const enabledKey = getEnabledKey(type);
const enabled = useExtract<boolean>(enabledKey); const enabled = useSettingValue<boolean>(enabledKey, { original: true });
const { setValue } = useFormActions(); const { setValue } = useFormActions();
const updateRow = useCallback( const updateRow = useCallback(

@ -1,3 +1,4 @@
import { LOG } from "@/utilities/console";
import { UseForm } from "@mantine/hooks/lib/use-form/use-form"; import { UseForm } from "@mantine/hooks/lib/use-form/use-form";
import { createContext, useCallback, useContext, useRef } from "react"; import { createContext, useCallback, useContext, useRef } from "react";
@ -26,6 +27,7 @@ export function useFormActions() {
const update = useCallback( const update = useCallback(
(object: LooseObject, location: FormKey = "settings") => { (object: LooseObject, location: FormKey = "settings") => {
LOG("info", `Updating values in ${location}`, object);
formRef.current.setValues((values) => { formRef.current.setValues((values) => {
const changes = { ...values[location], ...object }; const changes = { ...values[location], ...object };
return { ...values, [location]: changes }; return { ...values, [location]: changes };
@ -36,6 +38,7 @@ export function useFormActions() {
const setValue = useCallback( const setValue = useCallback(
(v: unknown, key: string, location: FormKey = "settings") => { (v: unknown, key: string, location: FormKey = "settings") => {
LOG("info", `Updating value of ${key} in ${location}`, v);
formRef.current.setValues((values) => { formRef.current.setValues((values) => {
const changes = { ...values[location], [key]: v }; const changes = { ...values[location], [key]: v };
return { ...values, [location]: changes }; return { ...values, [location]: changes };

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

@ -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 ?? ""; let callStack = error.stack ?? "";
// Remove sensitive information from the stack // Remove sensitive information from the stack
callStack = callStack.replaceAll(window.location.hostname, Placeholder); callStack = callStack.replaceAll(window.location.host, Placeholder);
return callStack; return callStack;
}, [error.stack]); }, [error.stack]);

Loading…
Cancel
Save