mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Plugin settings (#4143)
* Add backend support for plugin settings * Add plugin settings config * Add UI support for plugin settings
This commit is contained in:
@@ -13,6 +13,7 @@ import {
|
||||
useConfigureDLNA,
|
||||
useConfigureGeneral,
|
||||
useConfigureInterface,
|
||||
useConfigurePlugin,
|
||||
useConfigureScraping,
|
||||
useConfigureUI,
|
||||
} from "src/core/StashService";
|
||||
@@ -21,6 +22,7 @@ import { useToast } from "src/hooks/Toast";
|
||||
import { withoutTypename } from "src/utils/data";
|
||||
import { Icon } from "../Shared/Icon";
|
||||
|
||||
type PluginSettings = Record<string, Record<string, unknown>>;
|
||||
export interface ISettingsContextState {
|
||||
loading: boolean;
|
||||
error: ApolloError | undefined;
|
||||
@@ -30,6 +32,7 @@ export interface ISettingsContextState {
|
||||
scraping: GQL.ConfigScrapingInput;
|
||||
dlna: GQL.ConfigDlnaInput;
|
||||
ui: IUIConfig;
|
||||
plugins: PluginSettings;
|
||||
|
||||
// apikey isn't directly settable, so expose it here
|
||||
apiKey: string;
|
||||
@@ -40,28 +43,23 @@ export interface ISettingsContextState {
|
||||
saveScraping: (input: Partial<GQL.ConfigScrapingInput>) => void;
|
||||
saveDLNA: (input: Partial<GQL.ConfigDlnaInput>) => void;
|
||||
saveUI: (input: Partial<IUIConfig>) => void;
|
||||
savePluginSettings: (pluginID: string, input: {}) => void;
|
||||
|
||||
refetch: () => void;
|
||||
}
|
||||
|
||||
export const SettingStateContext = React.createContext<ISettingsContextState>({
|
||||
loading: false,
|
||||
error: undefined,
|
||||
general: {},
|
||||
interface: {},
|
||||
defaults: {},
|
||||
scraping: {},
|
||||
dlna: {},
|
||||
ui: {},
|
||||
apiKey: "",
|
||||
saveGeneral: () => {},
|
||||
saveInterface: () => {},
|
||||
saveDefaults: () => {},
|
||||
saveScraping: () => {},
|
||||
saveDLNA: () => {},
|
||||
saveUI: () => {},
|
||||
refetch: () => {},
|
||||
});
|
||||
export const SettingStateContext =
|
||||
React.createContext<ISettingsContextState | null>(null);
|
||||
|
||||
export const useSettings = () => {
|
||||
const context = React.useContext(SettingStateContext);
|
||||
|
||||
if (context === null) {
|
||||
throw new Error("useSettings must be used within a SettingsContext");
|
||||
}
|
||||
|
||||
return context;
|
||||
};
|
||||
|
||||
export const SettingsContext: React.FC = ({ children }) => {
|
||||
const Toast = useToast();
|
||||
@@ -97,6 +95,10 @@ export const SettingsContext: React.FC = ({ children }) => {
|
||||
const [pendingUI, setPendingUI] = useState<{}>();
|
||||
const [updateUIConfig] = useConfigureUI();
|
||||
|
||||
const [plugins, setPlugins] = useState<PluginSettings>({});
|
||||
const [pendingPlugins, setPendingPlugins] = useState<PluginSettings>();
|
||||
const [updatePluginConfig] = useConfigurePlugin();
|
||||
|
||||
const [updateSuccess, setUpdateSuccess] = useState<boolean>();
|
||||
|
||||
const [apiKey, setApiKey] = useState("");
|
||||
@@ -132,6 +134,7 @@ export const SettingsContext: React.FC = ({ children }) => {
|
||||
setScraping({ ...withoutTypename(data.configuration.scraping) });
|
||||
setDLNA({ ...withoutTypename(data.configuration.dlna) });
|
||||
setUI(data.configuration.ui);
|
||||
setPlugins(data.configuration.plugins);
|
||||
}, [data, error]);
|
||||
|
||||
const resetSuccess = useDebounce(() => setUpdateSuccess(undefined), 4000);
|
||||
@@ -433,6 +436,63 @@ export const SettingsContext: React.FC = ({ children }) => {
|
||||
});
|
||||
}
|
||||
|
||||
// saves the configuration if no further changes are made after a half second
|
||||
const savePluginConfig = useDebounce(async (input: PluginSettings) => {
|
||||
try {
|
||||
setUpdateSuccess(undefined);
|
||||
|
||||
for (const pluginID in input) {
|
||||
await updatePluginConfig({
|
||||
variables: {
|
||||
plugin_id: pluginID,
|
||||
input: input[pluginID],
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
setPendingPlugins(undefined);
|
||||
onSuccess();
|
||||
} catch (e) {
|
||||
setSaveError(e);
|
||||
}
|
||||
}, 500);
|
||||
|
||||
useEffect(() => {
|
||||
if (!pendingPlugins) {
|
||||
return;
|
||||
}
|
||||
|
||||
savePluginConfig(pendingPlugins);
|
||||
}, [pendingPlugins, savePluginConfig]);
|
||||
|
||||
function savePluginSettings(
|
||||
pluginID: string,
|
||||
input: Record<string, unknown>
|
||||
) {
|
||||
if (!plugins) {
|
||||
return;
|
||||
}
|
||||
|
||||
setPlugins({
|
||||
...plugins,
|
||||
[pluginID]: input,
|
||||
});
|
||||
|
||||
setPendingPlugins((current) => {
|
||||
if (!current) {
|
||||
// use full UI object to ensure nothing is wiped
|
||||
return {
|
||||
...plugins,
|
||||
[pluginID]: input,
|
||||
};
|
||||
}
|
||||
return {
|
||||
...current,
|
||||
[pluginID]: input,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function maybeRenderLoadingIndicator() {
|
||||
if (updateSuccess === false) {
|
||||
return (
|
||||
@@ -448,7 +508,8 @@ export const SettingsContext: React.FC = ({ children }) => {
|
||||
pendingDefaults ||
|
||||
pendingScraping ||
|
||||
pendingDLNA ||
|
||||
pendingUI
|
||||
pendingUI ||
|
||||
pendingPlugins
|
||||
) {
|
||||
return (
|
||||
<div className="loading-indicator">
|
||||
@@ -480,6 +541,7 @@ export const SettingsContext: React.FC = ({ children }) => {
|
||||
scraping,
|
||||
dlna,
|
||||
ui,
|
||||
plugins,
|
||||
saveGeneral,
|
||||
saveInterface,
|
||||
saveDefaults,
|
||||
@@ -487,6 +549,7 @@ export const SettingsContext: React.FC = ({ children }) => {
|
||||
saveDLNA,
|
||||
saveUI,
|
||||
refetch,
|
||||
savePluginSettings,
|
||||
}}
|
||||
>
|
||||
{maybeRenderLoadingIndicator()}
|
||||
|
||||
Reference in New Issue
Block a user