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) => void; saveInterface: (input: Partial) => void; saveDefaults: (input: Partial) => void; saveScraping: (input: Partial) => void; saveDLNA: (input: Partial) => void; saveUI: (input: Partial) => void; } export const SettingStateContext = React.createContext({ 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({}); const [pendingGeneral, setPendingGeneral] = useState< GQL.ConfigGeneralInput | undefined >(); const [updateGeneralConfig] = useConfigureGeneral(); const [iface, setIface] = useState({}); const [pendingInterface, setPendingInterface] = useState< GQL.ConfigInterfaceInput | undefined >(); const [updateInterfaceConfig] = useConfigureInterface(); const [defaults, setDefaults] = useState({}); const [pendingDefaults, setPendingDefaults] = useState< GQL.ConfigDefaultSettingsInput | undefined >(); const [updateDefaultsConfig] = useConfigureDefaults(); const [scraping, setScraping] = useState({}); const [pendingScraping, setPendingScraping] = useState< GQL.ConfigScrapingInput | undefined >(); const [updateScrapingConfig] = useConfigureScraping(); const [dlna, setDLNA] = useState({}); 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(); 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(); 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) { 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) { 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) { 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) { 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) { 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 (
); } if ( pendingGeneral || pendingInterface || pendingDefaults || pendingScraping || pendingDLNA || pendingUI ) { return (
Loading...
); } if (updateSuccess) { return (
); } } return ( {maybeRenderLoadingIndicator()} {children} ); };