mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
519 lines
12 KiB
TypeScript
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(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>
|
|
);
|
|
};
|