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:
WithoutPants
2024-06-11 13:18:45 +10:00
committed by GitHub
parent ed057c971f
commit 845d718c67
7 changed files with 526 additions and 426 deletions

View File

@@ -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>
);
};

View File

@@ -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>
);
};
});

View File

@@ -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;

View File

@@ -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 />;

View File

@@ -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`

View File

@@ -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;

View File

@@ -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