mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Plugin api improvements (#4935)
* Support hook into App component * Add hookable PluginSettings component * Add useSettings to plugin hooks * Make setting inputs hookable * Add hooks for performer details panel * Update docs
This commit is contained in:
@@ -50,6 +50,7 @@ import { PluginRoutes } from "./plugins";
|
||||
// import plugin_api to run code
|
||||
import "./pluginApi";
|
||||
import { ConnectionMonitor } from "./ConnectionMonitor";
|
||||
import { PatchFunction } from "./patch";
|
||||
|
||||
const Performers = lazyComponent(
|
||||
() => import("./components/Performers/Performers")
|
||||
@@ -144,6 +145,13 @@ function sortPlugins(plugins: PluginList) {
|
||||
return sorted;
|
||||
}
|
||||
|
||||
const AppContainer: React.FC<React.PropsWithChildren<{}>> = PatchFunction(
|
||||
"App",
|
||||
(props: React.PropsWithChildren<{}>) => {
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
) as React.FC;
|
||||
|
||||
export const App: React.FC = () => {
|
||||
const config = useConfiguration();
|
||||
const [saveUI] = useConfigureUI();
|
||||
@@ -357,6 +365,7 @@ export const App: React.FC = () => {
|
||||
const titleProps = makeTitleProps();
|
||||
|
||||
return (
|
||||
<AppContainer>
|
||||
<ErrorBoundary>
|
||||
{messages ? (
|
||||
<IntlProvider
|
||||
@@ -393,5 +402,6 @@ export const App: React.FC = () => {
|
||||
</IntlProvider>
|
||||
) : null}
|
||||
</ErrorBoundary>
|
||||
</AppContainer>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { TagLink } from "src/components/Shared/TagLink";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
FormatPenisLength,
|
||||
FormatWeight,
|
||||
} from "../PerformerList";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IPerformerDetails {
|
||||
performer: GQL.PerformerDataFragment;
|
||||
@@ -20,11 +21,15 @@ interface IPerformerDetails {
|
||||
fullWidth?: boolean;
|
||||
}
|
||||
|
||||
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
performer,
|
||||
collapsed,
|
||||
fullWidth,
|
||||
}) => {
|
||||
const PerformerDetailGroup: React.FC<PropsWithChildren<IPerformerDetails>> =
|
||||
PatchComponent("PerformerDetailsPanel.DetailGroup", ({ children }) => {
|
||||
return <div className="detail-group">{children}</div>;
|
||||
});
|
||||
|
||||
export const PerformerDetailsPanel: React.FC<IPerformerDetails> =
|
||||
PatchComponent("PerformerDetailsPanel", (props) => {
|
||||
const { performer, collapsed, fullWidth } = props;
|
||||
|
||||
// Network state
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -98,11 +103,13 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="detail-group">
|
||||
<PerformerDetailGroup {...props}>
|
||||
{performer.gender ? (
|
||||
<DetailItem
|
||||
id="gender"
|
||||
value={intl.formatMessage({ id: "gender_types." + performer.gender })}
|
||||
value={intl.formatMessage({
|
||||
id: "gender_types." + performer.gender,
|
||||
})}
|
||||
fullWidth={fullWidth}
|
||||
/>
|
||||
) : (
|
||||
@@ -184,13 +191,12 @@ export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
fullWidth={fullWidth}
|
||||
/>
|
||||
{maybeRenderExtraDetails()}
|
||||
</div>
|
||||
</PerformerDetailGroup>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
export const CompressedPerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
performer,
|
||||
}) => {
|
||||
export const CompressedPerformerDetailsPanel: React.FC<IPerformerDetails> =
|
||||
PatchComponent("CompressedPerformerDetailsPanel", ({ performer }) => {
|
||||
// Network state
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -247,4 +253,4 @@ export const CompressedPerformerDetailsPanel: React.FC<IPerformerDetails> = ({
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
||||
@@ -92,13 +92,10 @@ interface ISettingGroup {
|
||||
collapsedDefault?: boolean;
|
||||
}
|
||||
|
||||
export const SettingGroup: React.FC<PropsWithChildren<ISettingGroup>> = ({
|
||||
settingProps,
|
||||
topLevel,
|
||||
collapsible,
|
||||
collapsedDefault,
|
||||
children,
|
||||
}) => {
|
||||
export const SettingGroup: React.FC<PropsWithChildren<ISettingGroup>> =
|
||||
PatchComponent(
|
||||
"SettingGroup",
|
||||
({ settingProps, topLevel, collapsible, collapsedDefault, children }) => {
|
||||
const [open, setOpen] = useState(!collapsedDefault);
|
||||
|
||||
function renderCollapseButton() {
|
||||
@@ -145,7 +142,8 @@ export const SettingGroup: React.FC<PropsWithChildren<ISettingGroup>> = ({
|
||||
</Collapse>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface IBooleanSetting extends ISetting {
|
||||
id: string;
|
||||
@@ -153,7 +151,9 @@ interface IBooleanSetting extends ISetting {
|
||||
onChange: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const BooleanSetting: React.FC<IBooleanSetting> = (props) => {
|
||||
export const BooleanSetting: React.FC<IBooleanSetting> = PatchComponent(
|
||||
"BooleanSetting",
|
||||
(props) => {
|
||||
const { id, disabled, checked, onChange, ...settingProps } = props;
|
||||
|
||||
return (
|
||||
@@ -166,22 +166,18 @@ export const BooleanSetting: React.FC<IBooleanSetting> = (props) => {
|
||||
/>
|
||||
</Setting>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface ISelectSetting extends ISetting {
|
||||
value?: string | number | string[];
|
||||
onChange: (v: string) => void;
|
||||
}
|
||||
|
||||
export const SelectSetting: React.FC<PropsWithChildren<ISelectSetting>> = ({
|
||||
id,
|
||||
headingID,
|
||||
subHeadingID,
|
||||
value,
|
||||
children,
|
||||
onChange,
|
||||
advanced,
|
||||
}) => {
|
||||
export const SelectSetting: React.FC<PropsWithChildren<ISelectSetting>> =
|
||||
PatchComponent(
|
||||
"SelectSetting",
|
||||
({ id, headingID, subHeadingID, value, children, onChange, advanced }) => {
|
||||
return (
|
||||
<Setting
|
||||
advanced={advanced}
|
||||
@@ -199,7 +195,8 @@ export const SelectSetting: React.FC<PropsWithChildren<ISelectSetting>> = ({
|
||||
</Form.Control>
|
||||
</Setting>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface IDialogSetting<T> extends ISetting {
|
||||
buttonText?: string;
|
||||
@@ -208,8 +205,7 @@ interface IDialogSetting<T> extends ISetting {
|
||||
renderValue?: (v: T | undefined) => JSX.Element;
|
||||
onChange: () => void;
|
||||
}
|
||||
|
||||
export const ChangeButtonSetting = <T extends {}>(props: IDialogSetting<T>) => {
|
||||
const _ChangeButtonSetting = <T extends {}>(props: IDialogSetting<T>) => {
|
||||
const {
|
||||
id,
|
||||
className,
|
||||
@@ -266,6 +262,11 @@ export const ChangeButtonSetting = <T extends {}>(props: IDialogSetting<T>) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ChangeButtonSetting = PatchComponent(
|
||||
"ChangeButtonSetting",
|
||||
_ChangeButtonSetting
|
||||
) as typeof _ChangeButtonSetting;
|
||||
|
||||
export interface ISettingModal<T> {
|
||||
heading?: React.ReactNode;
|
||||
headingID?: string;
|
||||
@@ -283,7 +284,7 @@ export interface ISettingModal<T> {
|
||||
error?: string | undefined;
|
||||
}
|
||||
|
||||
export const SettingModal = <T extends {}>(props: ISettingModal<T>) => {
|
||||
const _SettingModal = <T extends {}>(props: ISettingModal<T>) => {
|
||||
const {
|
||||
heading,
|
||||
headingID,
|
||||
@@ -342,6 +343,11 @@ export const SettingModal = <T extends {}>(props: ISettingModal<T>) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const SettingModal = PatchComponent(
|
||||
"SettingModal",
|
||||
_SettingModal
|
||||
) as typeof _SettingModal;
|
||||
|
||||
interface IModalSetting<T> extends ISetting {
|
||||
value: T | undefined;
|
||||
buttonText?: string;
|
||||
@@ -357,7 +363,7 @@ interface IModalSetting<T> extends ISetting {
|
||||
validateChange?: (v: T) => void | undefined;
|
||||
}
|
||||
|
||||
export const ModalSetting = <T extends {}>(props: IModalSetting<T>) => {
|
||||
export const _ModalSetting = <T extends {}>(props: IModalSetting<T>) => {
|
||||
const {
|
||||
id,
|
||||
className,
|
||||
@@ -435,12 +441,19 @@ export const ModalSetting = <T extends {}>(props: IModalSetting<T>) => {
|
||||
);
|
||||
};
|
||||
|
||||
export const ModalSetting = PatchComponent(
|
||||
"ModalSetting",
|
||||
_ModalSetting
|
||||
) as typeof _ModalSetting;
|
||||
|
||||
interface IStringSetting extends ISetting {
|
||||
value: string | undefined;
|
||||
onChange: (v: string) => void;
|
||||
}
|
||||
|
||||
export const StringSetting: React.FC<IStringSetting> = (props) => {
|
||||
export const StringSetting: React.FC<IStringSetting> = PatchComponent(
|
||||
"StringSetting",
|
||||
(props) => {
|
||||
return (
|
||||
<ModalSetting<string>
|
||||
{...props}
|
||||
@@ -456,14 +469,17 @@ export const StringSetting: React.FC<IStringSetting> = (props) => {
|
||||
renderValue={(value) => <span>{value}</span>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface INumberSetting extends ISetting {
|
||||
value: number | undefined;
|
||||
onChange: (v: number) => void;
|
||||
}
|
||||
|
||||
export const NumberSetting: React.FC<INumberSetting> = (props) => {
|
||||
export const NumberSetting: React.FC<INumberSetting> = PatchComponent(
|
||||
"NumberSetting",
|
||||
(props) => {
|
||||
return (
|
||||
<ModalSetting<number>
|
||||
{...props}
|
||||
@@ -480,7 +496,8 @@ export const NumberSetting: React.FC<INumberSetting> = (props) => {
|
||||
renderValue={(value) => <span>{value}</span>}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface IStringListSetting extends ISetting {
|
||||
value: string[] | undefined;
|
||||
@@ -488,7 +505,9 @@ interface IStringListSetting extends ISetting {
|
||||
onChange: (v: string[]) => void;
|
||||
}
|
||||
|
||||
export const StringListSetting: React.FC<IStringListSetting> = (props) => {
|
||||
export const StringListSetting: React.FC<IStringListSetting> = PatchComponent(
|
||||
"StringListSetting",
|
||||
(props) => {
|
||||
return (
|
||||
<ModalSetting<string[]>
|
||||
{...props}
|
||||
@@ -509,14 +528,15 @@ export const StringListSetting: React.FC<IStringListSetting> = (props) => {
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
}
|
||||
);
|
||||
|
||||
interface IConstantSetting<T> extends ISetting {
|
||||
value?: T;
|
||||
renderValue?: (v: T | undefined) => JSX.Element;
|
||||
}
|
||||
|
||||
export const ConstantSetting = <T extends {}>(props: IConstantSetting<T>) => {
|
||||
export const _ConstantSetting = <T extends {}>(props: IConstantSetting<T>) => {
|
||||
const { id, headingID, subHeading, subHeadingID, renderValue, value } = props;
|
||||
const intl = useIntl();
|
||||
|
||||
@@ -539,3 +559,8 @@ export const ConstantSetting = <T extends {}>(props: IConstantSetting<T>) => {
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export const ConstantSetting = PatchComponent(
|
||||
"ConstantSetting",
|
||||
_ConstantSetting
|
||||
) as typeof _ConstantSetting;
|
||||
|
||||
@@ -27,6 +27,7 @@ import {
|
||||
InstalledPluginPackages,
|
||||
} from "./PluginPackageManager";
|
||||
import { ExternalLink } from "../Shared/ExternalLink";
|
||||
import { PatchComponent } from "src/patch";
|
||||
|
||||
interface IPluginSettingProps {
|
||||
pluginID: string;
|
||||
@@ -75,11 +76,38 @@ const PluginSetting: React.FC<IPluginSettingProps> = ({
|
||||
}
|
||||
};
|
||||
|
||||
const PluginSettings: React.FC<{
|
||||
pluginID: string;
|
||||
settings: GQL.PluginSetting[];
|
||||
}> = PatchComponent("PluginSettings", ({ pluginID, settings }) => {
|
||||
const { plugins, savePluginSettings } = useSettings();
|
||||
const pluginSettings = plugins[pluginID] ?? {};
|
||||
|
||||
return (
|
||||
<div className="plugin-settings">
|
||||
{settings.map((setting) => (
|
||||
<PluginSetting
|
||||
key={setting.name}
|
||||
pluginID={pluginID}
|
||||
setting={setting}
|
||||
value={pluginSettings[setting.name]}
|
||||
onChange={(v) =>
|
||||
savePluginSettings(pluginID, {
|
||||
...pluginSettings,
|
||||
[setting.name]: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const SettingsPluginsPanel: React.FC = () => {
|
||||
const Toast = useToast();
|
||||
const intl = useIntl();
|
||||
|
||||
const { loading: configLoading, plugins, savePluginSettings } = useSettings();
|
||||
const { loading: configLoading } = useSettings();
|
||||
const { data, loading } = usePlugins();
|
||||
|
||||
const [changedPluginID, setChangedPluginID] = React.useState<
|
||||
@@ -163,7 +191,10 @@ export const SettingsPluginsPanel: React.FC = () => {
|
||||
}
|
||||
>
|
||||
{renderPluginHooks(plugin.hooks ?? undefined)}
|
||||
{renderPluginSettings(plugin.id, plugin.settings ?? [])}
|
||||
<PluginSettings
|
||||
pluginID={plugin.id}
|
||||
settings={plugin.settings ?? []}
|
||||
/>
|
||||
</SettingGroup>
|
||||
));
|
||||
|
||||
@@ -208,37 +239,8 @@ export const SettingsPluginsPanel: React.FC = () => {
|
||||
);
|
||||
}
|
||||
|
||||
function renderPluginSettings(
|
||||
pluginID: string,
|
||||
settings: GQL.PluginSetting[]
|
||||
) {
|
||||
const pluginSettings = plugins[pluginID] ?? {};
|
||||
|
||||
return settings.map((setting) => (
|
||||
<PluginSetting
|
||||
key={setting.name}
|
||||
pluginID={pluginID}
|
||||
setting={setting}
|
||||
value={pluginSettings[setting.name]}
|
||||
onChange={(v) =>
|
||||
savePluginSettings(pluginID, {
|
||||
...pluginSettings,
|
||||
[setting.name]: v,
|
||||
})
|
||||
}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
return renderPlugins();
|
||||
}, [
|
||||
data?.plugins,
|
||||
intl,
|
||||
Toast,
|
||||
changedPluginID,
|
||||
plugins,
|
||||
savePluginSettings,
|
||||
]);
|
||||
}, [data?.plugins, intl, Toast, changedPluginID]);
|
||||
|
||||
if (loading || configLoading) return <LoadingIndicator />;
|
||||
|
||||
|
||||
@@ -139,6 +139,11 @@ Returns `void`.
|
||||
|
||||
#### Patchable components and functions
|
||||
|
||||
- `App`
|
||||
- `BooleanSetting`
|
||||
- `ChangeButtonSetting`
|
||||
- `CompressedPerformerDetailsPanel`
|
||||
- `ConstantSetting`
|
||||
- `CountrySelect`
|
||||
- `DateInput`
|
||||
- `FolderSelect`
|
||||
@@ -146,9 +151,13 @@ Returns `void`.
|
||||
- `GallerySelect`
|
||||
- `GallerySelect.sort`
|
||||
- `Icon`
|
||||
- `ModalSetting`
|
||||
- `MovieIDSelect`
|
||||
- `MovieSelect`
|
||||
- `MovieSelect.sort`
|
||||
- `NumberSetting`
|
||||
- `PerformerDetailsPanel`
|
||||
- `PerformerDetailsPanel.DetailGroup`
|
||||
- `PerformerIDSelect`
|
||||
- `PerformerSelect`
|
||||
- `PerformerSelect.sort`
|
||||
@@ -161,13 +170,20 @@ Returns `void`.
|
||||
- `SceneIDSelect`
|
||||
- `SceneSelect`
|
||||
- `SceneSelect.sort`
|
||||
- `SelectSetting`
|
||||
- `Setting`
|
||||
- `SettingModal`
|
||||
- `StringSetting`
|
||||
- `StringListSetting`
|
||||
- `StudioIDSelect`
|
||||
- `StudioSelect`
|
||||
- `StudioSelect.sort`
|
||||
- `TagIDSelect`
|
||||
- `TagSelect`
|
||||
- `TagSelect.sort`
|
||||
- `PluginSettings`
|
||||
- `Setting`
|
||||
- `SettingGroup`
|
||||
|
||||
### `PluginApi.Event`
|
||||
|
||||
|
||||
41
ui/v2.5/src/pluginApi.d.ts
vendored
41
ui/v2.5/src/pluginApi.d.ts
vendored
@@ -681,7 +681,18 @@ declare namespace PluginApi {
|
||||
"SceneCard.Details": React.FC<any>;
|
||||
"SceneCard.Overlays": React.FC<any>;
|
||||
"SceneCard.Image": React.FC<any>;
|
||||
SceneCard: React.FC<any>;
|
||||
PluginSettings: React.FC<any>;
|
||||
Setting: React.FC<any>;
|
||||
SettingGroup: React.FC<any>;
|
||||
BooleanSetting: React.FC<any>;
|
||||
SelectSetting: React.FC<any>;
|
||||
ChangeButtonSetting: React.FC<any>;
|
||||
SettingModal: React.FC<any>;
|
||||
ModalSetting: React.FC<any>;
|
||||
StringSetting: React.FC<any>;
|
||||
NumberSetting: React.FC<any>;
|
||||
StringListSetting: React.FC<any>;
|
||||
ConstantSetting: React.FC<any>;
|
||||
};
|
||||
namespace utils {
|
||||
namespace NavUtils {
|
||||
@@ -922,6 +933,34 @@ declare namespace PluginApi {
|
||||
success(message: JSX.Element | string): void;
|
||||
error(error: unknown): void;
|
||||
};
|
||||
|
||||
function useSettings(): {
|
||||
loading: boolean;
|
||||
error: any | undefined;
|
||||
general: any;
|
||||
interface: any;
|
||||
defaults: any;
|
||||
scraping: any;
|
||||
dlna: any;
|
||||
ui: any;
|
||||
plugins: any;
|
||||
|
||||
advancedMode: boolean;
|
||||
|
||||
// apikey isn't directly settable, so expose it here
|
||||
apiKey: string;
|
||||
|
||||
saveGeneral: (input: any) => void;
|
||||
saveInterface: (input: any) => void;
|
||||
saveDefaults: (input: any) => void;
|
||||
saveScraping: (input: any) => void;
|
||||
saveDLNA: (input: any) => void;
|
||||
saveUI: (input: any) => void;
|
||||
savePluginSettings: (pluginID: string, input: {}) => void;
|
||||
setAdvancedMode: (value: boolean) => void;
|
||||
|
||||
refetch: () => void;
|
||||
};
|
||||
}
|
||||
namespace patch {
|
||||
function before(target: string, fn: Function): void;
|
||||
|
||||
@@ -15,6 +15,7 @@ import { useSpriteInfo } from "./hooks/sprite";
|
||||
import { useToast } from "./hooks/Toast";
|
||||
import Event from "./hooks/event";
|
||||
import { before, instead, after, components, RegisterComponent } from "./patch";
|
||||
import { useSettings } from "./components/Settings/context";
|
||||
|
||||
// due to code splitting, some components may not have been loaded when a plugin
|
||||
// page is loaded. This function will load all components passed to it.
|
||||
@@ -92,6 +93,7 @@ export const PluginApi = {
|
||||
useLoadComponents,
|
||||
useSpriteInfo,
|
||||
useToast,
|
||||
useSettings,
|
||||
},
|
||||
patch: {
|
||||
// intercept the arguments of supported functions
|
||||
|
||||
Reference in New Issue
Block a user