Files
stash/ui/v2.5/src/components/Settings/context.tsx
2022-09-30 11:28:47 +10:00

519 lines
12 KiB
TypeScript

import { ApolloError } from "@apollo/client/errors";
import {
faCheckCircle,
faTimesCircle,
} from "@fortawesome/free-solid-svg-icons";
import debounce from "lodash-es/debounce";
import React, {
useState,
useEffect,
useMemo,
useCallback,
useRef,
} from "react";
import { Spinner } from "react-bootstrap";
import { IUIConfig } from "src/core/config";
import * as GQL from "src/core/generated-graphql";
import {
useConfiguration,
useConfigureDefaults,
useConfigureDLNA,
useConfigureGeneral,
useConfigureInterface,
useConfigureScraping,
useConfigureUI,
} from "src/core/StashService";
import { useToast } from "src/hooks";
import { withoutTypename } from "src/utils";
import { Icon } from "../Shared";
export interface ISettingsContextState {
loading: boolean;
error: ApolloError | undefined;
general: GQL.ConfigGeneralInput;
interface: GQL.ConfigInterfaceInput;
defaults: GQL.ConfigDefaultSettingsInput;
scraping: GQL.ConfigScrapingInput;
dlna: GQL.ConfigDlnaInput;
ui: IUIConfig;
// apikey isn't directly settable, so expose it here
apiKey: string;
saveGeneral: (input: Partial<GQL.ConfigGeneralInput>) => void;
saveInterface: (input: Partial<GQL.ConfigInterfaceInput>) => void;
saveDefaults: (input: Partial<GQL.ConfigDefaultSettingsInput>) => void;
saveScraping: (input: Partial<GQL.ConfigScrapingInput>) => void;
saveDLNA: (input: Partial<GQL.ConfigDlnaInput>) => void;
saveUI: (input: Partial<IUIConfig>) => void;
}
export const SettingStateContext = React.createContext<ISettingsContextState>({
loading: false,
error: undefined,
general: {},
interface: {},
defaults: {},
scraping: {},
dlna: {},
ui: {},
apiKey: "",
saveGeneral: () => {},
saveInterface: () => {},
saveDefaults: () => {},
saveScraping: () => {},
saveDLNA: () => {},
saveUI: () => {},
});
export const SettingsContext: React.FC = ({ children }) => {
const Toast = useToast();
const { data, error, loading } = useConfiguration();
const initialRef = useRef(false);
const [general, setGeneral] = useState<GQL.ConfigGeneralInput>({});
const [pendingGeneral, setPendingGeneral] = useState<
GQL.ConfigGeneralInput | undefined
>();
const [updateGeneralConfig] = useConfigureGeneral();
const [iface, setIface] = useState<GQL.ConfigInterfaceInput>({});
const [pendingInterface, setPendingInterface] = useState<
GQL.ConfigInterfaceInput | undefined
>();
const [updateInterfaceConfig] = useConfigureInterface();
const [defaults, setDefaults] = useState<GQL.ConfigDefaultSettingsInput>({});
const [pendingDefaults, setPendingDefaults] = useState<
GQL.ConfigDefaultSettingsInput | undefined
>();
const [updateDefaultsConfig] = useConfigureDefaults();
const [scraping, setScraping] = useState<GQL.ConfigScrapingInput>({});
const [pendingScraping, setPendingScraping] = useState<
GQL.ConfigScrapingInput | undefined
>();
const [updateScrapingConfig] = useConfigureScraping();
const [dlna, setDLNA] = useState<GQL.ConfigDlnaInput>({});
const [pendingDLNA, setPendingDLNA] = useState<
GQL.ConfigDlnaInput | undefined
>();
const [updateDLNAConfig] = useConfigureDLNA();
const [ui, setUI] = useState({});
const [pendingUI, setPendingUI] = useState<{} | undefined>();
const [updateUIConfig] = useConfigureUI();
const [updateSuccess, setUpdateSuccess] = useState<boolean | undefined>();
const [apiKey, setApiKey] = useState("");
// cannot use Toast.error directly with the debounce functions
// since they are refreshed every time the Toast context is updated.
const [saveError, setSaveError] = useState<unknown>();
useEffect(() => {
if (!saveError) {
return;
}
Toast.error(saveError);
setSaveError(undefined);
setUpdateSuccess(false);
}, [saveError, Toast]);
useEffect(() => {
// only initialise once - assume we have control over these settings and
// they aren't modified elsewhere
if (!data?.configuration || error || initialRef.current) return;
initialRef.current = true;
setGeneral({ ...withoutTypename(data.configuration.general) });
setIface({ ...withoutTypename(data.configuration.interface) });
setDefaults({ ...withoutTypename(data.configuration.defaults) });
setScraping({ ...withoutTypename(data.configuration.scraping) });
setDLNA({ ...withoutTypename(data.configuration.dlna) });
setUI({ ...withoutTypename(data.configuration.ui) });
setApiKey(data.configuration.general.apiKey);
}, [data, error]);
const resetSuccess = useMemo(
() =>
debounce(() => {
setUpdateSuccess(undefined);
}, 4000),
[]
);
const onSuccess = useCallback(() => {
setUpdateSuccess(true);
resetSuccess();
}, [resetSuccess]);
// saves the configuration if no further changes are made after a half second
const saveGeneralConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigGeneralInput) => {
try {
setUpdateSuccess(undefined);
await updateGeneralConfig({
variables: {
input,
},
});
setPendingGeneral(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateGeneralConfig, onSuccess]
);
useEffect(() => {
if (!pendingGeneral) {
return;
}
saveGeneralConfig(pendingGeneral);
}, [pendingGeneral, saveGeneralConfig]);
function saveGeneral(input: Partial<GQL.ConfigGeneralInput>) {
if (!general) {
return;
}
setGeneral({
...general,
...input,
});
setPendingGeneral((current) => {
if (!current) {
return input;
}
return {
...current,
...input,
};
});
}
// saves the configuration if no further changes are made after a half second
const saveInterfaceConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigInterfaceInput) => {
try {
setUpdateSuccess(undefined);
await updateInterfaceConfig({
variables: {
input,
},
});
setPendingInterface(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateInterfaceConfig, onSuccess]
);
useEffect(() => {
if (!pendingInterface) {
return;
}
saveInterfaceConfig(pendingInterface);
}, [pendingInterface, saveInterfaceConfig]);
function saveInterface(input: Partial<GQL.ConfigInterfaceInput>) {
if (!iface) {
return;
}
setIface({
...iface,
...input,
});
setPendingInterface((current) => {
if (!current) {
return input;
}
return {
...current,
...input,
};
});
}
// saves the configuration if no further changes are made after a half second
const saveDefaultsConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigDefaultSettingsInput) => {
try {
setUpdateSuccess(undefined);
await updateDefaultsConfig({
variables: {
input,
},
});
setPendingDefaults(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateDefaultsConfig, onSuccess]
);
useEffect(() => {
if (!pendingDefaults) {
return;
}
saveDefaultsConfig(pendingDefaults);
}, [pendingDefaults, saveDefaultsConfig]);
function saveDefaults(input: Partial<GQL.ConfigDefaultSettingsInput>) {
if (!defaults) {
return;
}
setDefaults({
...defaults,
...input,
});
setPendingDefaults((current) => {
if (!current) {
return input;
}
return {
...current,
...input,
};
});
}
// saves the configuration if no further changes are made after a half second
const saveScrapingConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigScrapingInput) => {
try {
setUpdateSuccess(undefined);
await updateScrapingConfig({
variables: {
input,
},
});
setPendingScraping(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateScrapingConfig, onSuccess]
);
useEffect(() => {
if (!pendingScraping) {
return;
}
saveScrapingConfig(pendingScraping);
}, [pendingScraping, saveScrapingConfig]);
function saveScraping(input: Partial<GQL.ConfigScrapingInput>) {
if (!scraping) {
return;
}
setScraping({
...scraping,
...input,
});
setPendingScraping((current) => {
if (!current) {
return input;
}
return {
...current,
...input,
};
});
}
// saves the configuration if no further changes are made after a half second
const saveDLNAConfig = useMemo(
() =>
debounce(async (input: GQL.ConfigDlnaInput) => {
try {
setUpdateSuccess(undefined);
await updateDLNAConfig({
variables: {
input,
},
});
setPendingDLNA(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateDLNAConfig, onSuccess]
);
useEffect(() => {
if (!pendingDLNA) {
return;
}
saveDLNAConfig(pendingDLNA);
}, [pendingDLNA, saveDLNAConfig]);
function saveDLNA(input: Partial<GQL.ConfigDlnaInput>) {
if (!dlna) {
return;
}
setDLNA({
...dlna,
...input,
});
setPendingDLNA((current) => {
if (!current) {
return input;
}
return {
...current,
...input,
};
});
}
// saves the configuration if no further changes are made after a half second
const saveUIConfig = useMemo(
() =>
debounce(async (input: IUIConfig) => {
try {
setUpdateSuccess(undefined);
await updateUIConfig({
variables: {
input,
},
});
setPendingUI(undefined);
onSuccess();
} catch (e) {
setSaveError(e);
}
}, 500),
[updateUIConfig, onSuccess]
);
useEffect(() => {
if (!pendingUI) {
return;
}
saveUIConfig(pendingUI);
}, [pendingUI, saveUIConfig]);
function saveUI(input: IUIConfig) {
if (!ui) {
return;
}
setUI({
...ui,
...input,
});
setPendingUI((current) => {
if (!current) {
// use full UI object to ensure nothing is wiped
return {
...ui,
...input,
};
}
return {
...current,
...input,
};
});
}
function maybeRenderLoadingIndicator() {
if (updateSuccess === false) {
return (
<div className="loading-indicator failed">
<Icon icon={faTimesCircle} className="fa-fw" />
</div>
);
}
if (
pendingGeneral ||
pendingInterface ||
pendingDefaults ||
pendingScraping ||
pendingDLNA ||
pendingUI
) {
return (
<div className="loading-indicator">
<Spinner animation="border" role="status">
<span className="sr-only">Loading...</span>
</Spinner>
</div>
);
}
if (updateSuccess) {
return (
<div className="loading-indicator success">
<Icon icon={faCheckCircle} className="fa-fw" />
</div>
);
}
}
return (
<SettingStateContext.Provider
value={{
loading,
error,
apiKey,
general,
interface: iface,
defaults,
scraping,
dlna,
ui,
saveGeneral,
saveInterface,
saveDefaults,
saveScraping,
saveDLNA,
saveUI,
}}
>
{maybeRenderLoadingIndicator()}
{children}
</SettingStateContext.Provider>
);
};