diff --git a/frontend/src/pages/Settings/General/index.tsx b/frontend/src/pages/Settings/General/index.tsx
index cb50f7537..175278dc4 100644
--- a/frontend/src/pages/Settings/General/index.tsx
+++ b/frontend/src/pages/Settings/General/index.tsx
@@ -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,
+ }}
>
Reverse proxy support
@@ -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),
+ }}
>
@@ -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),
+ }}
>
,
@@ -41,7 +42,7 @@ export const LanguageSelector: FunctionComponent<
};
export const ProfileSelector: FunctionComponent<
- Omit, "beforeStaged" | "options" | "clearable">
+ Omit, "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) }}
>
);
};
diff --git a/frontend/src/pages/Settings/Languages/index.tsx b/frontend/src/pages/Settings/Languages/index.tsx
index 2fc744588..726992916 100644
--- a/frontend/src/pages/Settings/Languages/index.tsx
+++ b/frontend/src/pages/Settings/Languages/index.tsx
@@ -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";
diff --git a/frontend/src/pages/Settings/Notifications/components.tsx b/frontend/src/pages/Settings/Notifications/components.tsx
index e6185dc9b..5f1f833d9 100644
--- a/frontend/src/pages/Settings/Notifications/components.tsx
+++ b/frontend/src/pages/Settings/Notifications/components.tsx
@@ -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(
+ const notifications = useSettingValue(
notificationsKey,
- "name",
- (s) => s.notifications.providers
+ {
+ onLoaded: (settings) => settings.notifications.providers,
+ onSubmit: (value) => value.map((v) => JSON.stringify(v)),
+ }
);
const update = useUpdateArray(
diff --git a/frontend/src/pages/Settings/Providers/components.tsx b/frontend/src/pages/Settings/Providers/components.tsx
index 8fc6575ea..da3628c8b 100644
--- a/frontend/src/pages/Settings/Providers/components.tsx
+++ b/frontend/src/pages/Settings/Providers/components.tsx
@@ -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";
diff --git a/frontend/src/pages/Settings/Radarr/index.tsx b/frontend/src/pages/Settings/Radarr/index.tsx
index f76cb8dba..41f186a25 100644
--- a/frontend/src/pages/Settings/Radarr/index.tsx
+++ b/frontend/src/pages/Settings/Radarr/index.tsx
@@ -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 (
@@ -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,
+ }}
>
diff --git a/frontend/src/pages/Settings/Sonarr/index.tsx b/frontend/src/pages/Settings/Sonarr/index.tsx
index f388dc039..2f5490392 100644
--- a/frontend/src/pages/Settings/Sonarr/index.tsx
+++ b/frontend/src/pages/Settings/Sonarr/index.tsx
@@ -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 (
@@ -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,
+ }}
>
diff --git a/frontend/src/pages/Settings/Subtitles/index.tsx b/frontend/src/pages/Settings/Subtitles/index.tsx
index f06dfa1f8..adb0b56d5 100644
--- a/frontend/src/pages/Settings/Subtitles/index.tsx
+++ b/frontend/src/pages/Settings/Subtitles/index.tsx
@@ -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}
>
Choose the anti-captcha provider you want to use
@@ -224,7 +216,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
(v === undefined ? "3w" : v)}
+ settingOptions={{ onSaved: (v) => (v === undefined ? "3w" : v) }}
options={adaptiveSearchingDelayOption}
>
@@ -233,7 +225,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
(v === undefined ? "1w" : v)}
+ settingOptions={{ onSaved: (v) => (v === undefined ? "1w" : v) }}
options={adaptiveSearchingDeltaOption}
>
@@ -299,7 +291,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
@@ -308,7 +300,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
@@ -317,7 +309,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
@@ -326,7 +318,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
@@ -334,7 +326,9 @@ const SettingsSubtitlesView: FunctionComponent = () => {
@@ -345,7 +339,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
label="Color"
clearable
options={colorOptions}
- override={subzeroColorOverride}
+ settingOptions={{ onLoaded: SubzeroColorModification }}
settingKey="subzero-color"
>
@@ -355,7 +349,7 @@ const SettingsSubtitlesView: FunctionComponent = () => {
diff --git a/frontend/src/pages/Settings/UI/index.tsx b/frontend/src/pages/Settings/UI/index.tsx
index dadce55c8..2f03e8c18 100644
--- a/frontend/src/pages/Settings/UI/index.tsx
+++ b/frontend/src/pages/Settings/UI/index.tsx
@@ -13,7 +13,7 @@ const SettingsUIView: FunctionComponent = () => {
options={pageSizeOptions}
location="storages"
settingKey={uiPageSizeKey}
- override={(_) => pageSize}
+ settingOptions={{ onLoaded: () => pageSize }}
>
diff --git a/frontend/src/pages/Settings/components/Layout.tsx b/frontend/src/pages/Settings/components/Layout.tsx
index 5e6949a28..d79cd2eec 100644
--- a/frontend/src/pages/Settings/components/Layout.tsx
+++ b/frontend/src/pages/Settings/components/Layout.tsx
@@ -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) => {
if (Object.keys(settings).length > 0) {
const settingsToSubmit = { ...settings };
- submitHooks(settingsToSubmit);
+ invokeHooks(settingsToSubmit);
LOG("info", "submitting settings", settingsToSubmit);
mutate(settingsToSubmit);
}
diff --git a/frontend/src/pages/Settings/components/collapse.tsx b/frontend/src/pages/Settings/components/collapse.tsx
index 32b582496..8f224f109 100644
--- a/frontend/src/pages/Settings/components/collapse.tsx
+++ b/frontend/src/pages/Settings/components/collapse.tsx
@@ -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;
diff --git a/frontend/src/pages/Settings/components/forms.tsx b/frontend/src/pages/Settings/components/forms.tsx
index e0d880db9..09cb18789 100644
--- a/frontend/src/pages/Settings/components/forms.tsx
+++ b/frontend/src/pages/Settings/components/forms.tsx
@@ -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 {
- disabled?: boolean;
- settingKey: string;
- location?: FormKey;
- override?: OverrideFuncType;
- beforeStaged?: (v: T) => unknown;
-}
+import { FunctionComponent, ReactText } from "react";
+import { BaseInput, useBaseInput } from "../utilities/hooks";
export type NumberProps = BaseInput & NumberInputProps;
-export const Number: FunctionComponent = ({
- beforeStaged,
- override,
- settingKey,
- location,
- ...props
-}) => {
- const value = useSettingValue(settingKey, override);
- const { setValue } = useFormActions();
+export const Number: FunctionComponent = (props) => {
+ const { value, update, rest } = useBaseInput(props);
return (
{
- const value = beforeStaged ? beforeStaged(val) : val;
- setValue(value, settingKey, location);
+ update(val);
}}
>
);
@@ -61,24 +43,15 @@ export const Number: FunctionComponent = ({
export type TextProps = BaseInput & TextInputProps;
-export const Text: FunctionComponent = ({
- beforeStaged,
- override,
- settingKey,
- location,
- ...props
-}) => {
- const value = useSettingValue(settingKey, override);
- const { setValue } = useFormActions();
+export const Text: FunctionComponent = (props) => {
+ const { value, update, rest } = useBaseInput(props);
return (
{
- const val = e.currentTarget.value;
- const value = beforeStaged ? beforeStaged(val) : val;
- setValue(value, settingKey, location);
+ update(e.currentTarget.value);
}}
>
);
@@ -86,24 +59,15 @@ export const Text: FunctionComponent = ({
export type PasswordProps = BaseInput & PasswordInputProps;
-export const Password: FunctionComponent = ({
- settingKey,
- location,
- override,
- beforeStaged,
- ...props
-}) => {
- const value = useSettingValue(settingKey, override);
- const { setValue } = useFormActions();
+export const Password: FunctionComponent = (props) => {
+ const { value, update, rest } = useBaseInput(props);
return (
{
- const val = e.currentTarget.value;
- const value = beforeStaged ? beforeStaged(val) : val;
- setValue(value, settingKey, location);
+ update(e.currentTarget.value);
}}
>
);
@@ -116,23 +80,18 @@ export interface CheckProps extends BaseInput {
export const Check: FunctionComponent = ({
label,
- override,
- disabled,
- settingKey,
- location,
+ inline,
+ ...props
}) => {
- const value = useSettingValue(settingKey, override);
- const { setValue } = useFormActions();
+ const { value, update, rest } = useBaseInput(props);
return (
{
- const { checked } = e.currentTarget;
- setValue(checked, settingKey, location);
+ update(e.currentTarget.checked);
}}
- disabled={disabled}
+ disabled={rest.disabled}
checked={value ?? false}
>
);
@@ -142,20 +101,10 @@ export type SelectorProps = BaseInput &
GlobalSelectorProps;
export function Selector(props: SelectorProps) {
- const { settingKey, location, override, beforeStaged, ...selector } = props;
-
- const value = useSettingValue(settingKey, override);
- const { setValue } = useFormActions();
+ const { value, update, rest } = useBaseInput(props);
return (
- {
- const result = beforeStaged && v ? beforeStaged(v) : v;
- setValue(result, settingKey, location);
- }}
- >
+
);
}
@@ -165,19 +114,13 @@ export type MultiSelectorProps = BaseInput &
export function MultiSelector(
props: MultiSelectorProps
) {
- const { settingKey, location, override, beforeStaged, ...selector } = props;
-
- const value = useSettingValue(settingKey, override);
- const { setValue } = useFormActions();
+ const { value, update, rest } = useBaseInput(props);
return (
{
- const result = beforeStaged && v ? beforeStaged(v) : v;
- setValue(result, settingKey, location);
- }}
+ onChange={update}
>
);
}
@@ -186,22 +129,19 @@ type SliderProps = BaseInput &
Omit;
export const Slider: FunctionComponent = (props) => {
- const { settingKey, location, override, label, ...slider } = props;
+ const { value, update, rest } = useBaseInput(props);
- const value = useSettingValue(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 (
-
+
{
- setValue(v, settingKey, location);
- }}
+ onChange={update}
value={value ?? 0}
- {...slider}
>
);
@@ -211,47 +151,28 @@ type ChipsProp = BaseInput &
Omit;
export const Chips: FunctionComponent = (props) => {
- const { settingKey, location, override, ...chips } = props;
-
- const value = useSettingValue(settingKey, override);
- const { setValue } = useFormActions();
+ const { value, update, rest } = useBaseInput(props);
return (
- {
- setValue(v, settingKey, location);
- }}
- {...chips}
- >
+
);
};
type ActionProps = {
- onClick?: (update: (v: unknown) => void, value?: string) => void;
-} & Omit, "override" | "beforeStaged">;
+ onClick?: (update: (v: string) => void, value?: string) => void;
+} & Omit, "modification">;
export const Action: FunctionComponent<
Override
> = (props) => {
- const { onClick, settingKey, location, ...button } = props;
-
- const value = useSettingValue(settingKey);
- const { setValue } = useFormActions();
-
- const wrappedSetValue = useCallback(
- (v: unknown) => {
- setValue(v, settingKey, location);
- },
- [location, setValue, settingKey]
- );
+ const { value, update, rest } = useBaseInput(props);
return (
{
- onClick?.(wrappedSetValue, value ?? undefined);
+ props.onClick?.(update, (value as string) ?? undefined);
}}
- {...button}
>
);
};
@@ -261,17 +182,13 @@ interface FileProps extends BaseInput {}
export const File: FunctionComponent> = (
props
) => {
- const { settingKey, location, override, ...file } = props;
- const value = useSettingValue(settingKey);
- const { setValue } = useFormActions();
+ const { value, update, rest } = useBaseInput(props);
return (
{
- setValue(p, settingKey, location);
- }}
- {...file}
+ onChange={update}
>
);
};
diff --git a/frontend/src/pages/Settings/components/hooks.ts b/frontend/src/pages/Settings/components/hooks.ts
deleted file mode 100644
index 4793a4a62..000000000
--- a/frontend/src/pages/Settings/components/hooks.ts
+++ /dev/null
@@ -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 = (settings: Settings) => T;
-
-export function useExtract(
- key: string,
- override?: OverrideFuncType
-): Readonly> {
- 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;
-
- return value;
- }, [key, settings]);
-
- return extractValue;
-}
-
-export function useSettingValue(
- key: string,
- override?: OverrideFuncType
-): Readonly> {
- const extractValue = useExtract(key, override);
- const stagedValue = useStagedValues();
- if (key in stagedValue) {
- return stagedValue[key] as T;
- } else {
- return extractValue;
- }
-}
-
-export function useLatestArray(
- key: string,
- compare: keyof T,
- override?: OverrideFuncType
-): Readonly> {
- const extractValue = useExtract(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(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]
- );
-}
diff --git a/frontend/src/pages/Settings/components/index.tsx b/frontend/src/pages/Settings/components/index.tsx
index 9df168a5d..437e6948d 100644
--- a/frontend/src/pages/Settings/components/index.tsx
+++ b/frontend/src/pages/Settings/components/index.tsx
@@ -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";
diff --git a/frontend/src/pages/Settings/components/pathMapper.tsx b/frontend/src/pages/Settings/components/pathMapper.tsx
index 6c11139c8..a7e76594b 100644
--- a/frontend/src/pages/Settings/components/pathMapper.tsx
+++ b/frontend/src/pages/Settings/components/pathMapper.tsx
@@ -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 = ({ type }) => {
const items = useSettingValue<[string, string][]>(key);
const enabledKey = getEnabledKey(type);
- const enabled = useExtract(enabledKey);
+ const enabled = useSettingValue(enabledKey, { original: true });
+
const { setValue } = useFormActions();
const updateRow = useCallback(
diff --git a/frontend/src/pages/Settings/utilities/FormValues.ts b/frontend/src/pages/Settings/utilities/FormValues.ts
index d2a6618ea..caf11a282 100644
--- a/frontend/src/pages/Settings/utilities/FormValues.ts
+++ b/frontend/src/pages/Settings/utilities/FormValues.ts
@@ -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 };
diff --git a/frontend/src/pages/Settings/utilities/hooks.ts b/frontend/src/pages/Settings/utilities/hooks.ts
new file mode 100644
index 000000000..09f054cc3
--- /dev/null
+++ b/frontend/src/pages/Settings/utilities/hooks.ts
@@ -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 {
+ disabled?: boolean;
+ settingKey: string;
+ location?: FormKey;
+ settingOptions?: SettingValueOptions;
+}
+
+export type SettingValueOptions = {
+ original?: boolean;
+ defaultValue?: T;
+ onLoaded?: (settings: Settings) => T;
+ onSaved?: (value: T) => unknown;
+ onSubmit?: (value: T) => unknown;
+};
+
+export function useBaseInput(props: T & BaseInput) {
+ const { settingKey, settingOptions, location, ...rest } = props;
+ // TODO: Opti options
+ const value = useSettingValue(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(
+ key: string,
+ options?: SettingValueOptions
+): Readonly> {
+ 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;
+
+ 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(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]
+ );
+}
diff --git a/frontend/src/pages/Settings/utilities/modifications.ts b/frontend/src/pages/Settings/utilities/modifications.ts
new file mode 100644
index 000000000..61abc4f91
--- /dev/null
+++ b/frontend/src/pages/Settings/utilities/modifications.ts
@@ -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")) ?? "";
diff --git a/frontend/src/pages/UIError.tsx b/frontend/src/pages/UIError.tsx
index 7f84ab91b..4f26d0d0c 100644
--- a/frontend/src/pages/UIError.tsx
+++ b/frontend/src/pages/UIError.tsx
@@ -26,7 +26,7 @@ const UIError: FunctionComponent = ({ 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]);