diff --git a/ui/v2.5/src/App.tsx b/ui/v2.5/src/App.tsx index d670786af..b3ff5e10f 100644 --- a/ui/v2.5/src/App.tsx +++ b/ui/v2.5/src/App.tsx @@ -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> = PatchFunction( + "App", + (props: React.PropsWithChildren<{}>) => { + return <>{props.children}; + } +) as React.FC; + export const App: React.FC = () => { const config = useConfiguration(); const [saveUI] = useConfigureUI(); @@ -357,41 +365,43 @@ export const App: React.FC = () => { const titleProps = makeTitleProps(); return ( - - {messages ? ( - - + + {messages ? ( + - {maybeRenderReleaseNotes()} - - - }> - - - - - {maybeRenderNavbar()} -
- {renderContent()} -
-
-
-
-
-
-
-
- ) : null} -
+ + {maybeRenderReleaseNotes()} + + + }> + + + + + {maybeRenderNavbar()} +
+ {renderContent()} +
+
+
+
+
+
+
+ + ) : null} + + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx index 2a330a6f1..94e27a362 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -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,231 +21,236 @@ interface IPerformerDetails { fullWidth?: boolean; } -export const PerformerDetailsPanel: React.FC = ({ - performer, - collapsed, - fullWidth, -}) => { - // Network state - const intl = useIntl(); +const PerformerDetailGroup: React.FC> = + PatchComponent("PerformerDetailsPanel.DetailGroup", ({ children }) => { + return
{children}
; + }); - function renderTagsField() { - if (!performer.tags.length) { - return; - } - return ( -
    - {(performer.tags ?? []).map((tag) => ( - - ))} -
- ); - } +export const PerformerDetailsPanel: React.FC = + PatchComponent("PerformerDetailsPanel", (props) => { + const { performer, collapsed, fullWidth } = props; - function renderStashIDs() { - if (!performer.stash_ids.length) { - return; - } + // Network state + const intl = useIntl(); - return ( -
    - {performer.stash_ids.map((stashID) => ( -
  • - -
  • - ))} -
- ); - } - - function maybeRenderExtraDetails() { - if (!collapsed) { - /* Remove extra urls provided in details since they will be present by perfomr name */ - /* This code can be removed once multple urls are supported for performers */ - let details = performer?.details - ?.replace(/\[((?:http|www\.)[^\n\]]+)\]/gm, "") - .trim(); + function renderTagsField() { + if (!performer.tags.length) { + return; + } return ( - <> - - - - - - - +
    + {(performer.tags ?? []).map((tag) => ( + + ))} +
); } - } - return ( -
- {performer.gender ? ( - - ) : ( - "" - )} - - - {performer.country ? ( - + {performer.stash_ids.map((stashID) => ( +
  • + +
  • + ))} + + ); + } + + function maybeRenderExtraDetails() { + if (!collapsed) { + /* Remove extra urls provided in details since they will be present by perfomr name */ + /* This code can be removed once multple urls are supported for performers */ + let details = performer?.details + ?.replace(/\[((?:http|www\.)[^\n\]]+)\]/gm, "") + .trim(); + return ( + <> + + + + + + + + ); + } + } + + return ( + + {performer.gender ? ( + + ) : ( + "" + )} + - ) : ( - "" - )} - - - - - - - - - - {maybeRenderExtraDetails()} -
    - ); -}; - -export const CompressedPerformerDetailsPanel: React.FC = ({ - performer, -}) => { - // Network state - const intl = useIntl(); - - function scrollToTop() { - window.scrollTo({ top: 0, behavior: "smooth" }); - } - - return ( -
    -
    - scrollToTop()}> - {performer.name} - - {performer.gender ? ( - <> - / - - {intl.formatMessage({ id: "gender_types." + performer.gender })} - - - ) : ( - "" - )} - {performer.birthdate ? ( - <> - / - - {TextUtils.age(performer.birthdate, performer.death_date)} - - - ) : ( - "" - )} + {performer.country ? ( - <> - / - + - - + } + fullWidth={fullWidth} + /> ) : ( "" )} + + + + + + + + + + {maybeRenderExtraDetails()} + + ); + }); + +export const CompressedPerformerDetailsPanel: React.FC = + PatchComponent("CompressedPerformerDetailsPanel", ({ performer }) => { + // Network state + const intl = useIntl(); + + function scrollToTop() { + window.scrollTo({ top: 0, behavior: "smooth" }); + } + + return ( +
    +
    + scrollToTop()}> + {performer.name} + + {performer.gender ? ( + <> + / + + {intl.formatMessage({ id: "gender_types." + performer.gender })} + + + ) : ( + "" + )} + {performer.birthdate ? ( + <> + / + + {TextUtils.age(performer.birthdate, performer.death_date)} + + + ) : ( + "" + )} + {performer.country ? ( + <> + / + + + + + ) : ( + "" + )} +
    -
    - ); -}; + ); + }); diff --git a/ui/v2.5/src/components/Settings/Inputs.tsx b/ui/v2.5/src/components/Settings/Inputs.tsx index 61353011f..073be4e11 100644 --- a/ui/v2.5/src/components/Settings/Inputs.tsx +++ b/ui/v2.5/src/components/Settings/Inputs.tsx @@ -92,60 +92,58 @@ interface ISettingGroup { collapsedDefault?: boolean; } -export const SettingGroup: React.FC> = ({ - settingProps, - topLevel, - collapsible, - collapsedDefault, - children, -}) => { - const [open, setOpen] = useState(!collapsedDefault); +export const SettingGroup: React.FC> = + PatchComponent( + "SettingGroup", + ({ settingProps, topLevel, collapsible, collapsedDefault, children }) => { + const [open, setOpen] = useState(!collapsedDefault); - function renderCollapseButton() { - if (!collapsible) return; + function renderCollapseButton() { + if (!collapsible) return; - return ( - - ); - } - - function onDivClick(e: React.MouseEvent) { - if (!collapsible) return; - - // ensure button was not clicked - let target: HTMLElement | null = e.target as HTMLElement; - while (target && target !== e.currentTarget) { - if ( - target.nodeName.toLowerCase() === "button" || - target.nodeName.toLowerCase() === "a" - ) { - // button clicked, swallow event - return; + return ( + + ); } - target = target.parentElement; + + function onDivClick(e: React.MouseEvent) { + if (!collapsible) return; + + // ensure button was not clicked + let target: HTMLElement | null = e.target as HTMLElement; + while (target && target !== e.currentTarget) { + if ( + target.nodeName.toLowerCase() === "button" || + target.nodeName.toLowerCase() === "a" + ) { + // button clicked, swallow event + return; + } + target = target.parentElement; + } + + setOpen(!open); + } + + return ( +
    + + {topLevel} + {renderCollapseButton()} + + +
    {children}
    +
    +
    + ); } - - setOpen(!open); - } - - return ( -
    - - {topLevel} - {renderCollapseButton()} - - -
    {children}
    -
    -
    ); -}; interface IBooleanSetting extends ISetting { id: string; @@ -153,53 +151,52 @@ interface IBooleanSetting extends ISetting { onChange: (v: boolean) => void; } -export const BooleanSetting: React.FC = (props) => { - const { id, disabled, checked, onChange, ...settingProps } = props; +export const BooleanSetting: React.FC = PatchComponent( + "BooleanSetting", + (props) => { + const { id, disabled, checked, onChange, ...settingProps } = props; - return ( - - onChange(!checked)} - /> - - ); -}; + return ( + + onChange(!checked)} + /> + + ); + } +); interface ISelectSetting extends ISetting { value?: string | number | string[]; onChange: (v: string) => void; } -export const SelectSetting: React.FC> = ({ - id, - headingID, - subHeadingID, - value, - children, - onChange, - advanced, -}) => { - return ( - - onChange(e.currentTarget.value)} - > - {children} - - +export const SelectSetting: React.FC> = + PatchComponent( + "SelectSetting", + ({ id, headingID, subHeadingID, value, children, onChange, advanced }) => { + return ( + + onChange(e.currentTarget.value)} + > + {children} + + + ); + } ); -}; interface IDialogSetting extends ISetting { buttonText?: string; @@ -208,8 +205,7 @@ interface IDialogSetting extends ISetting { renderValue?: (v: T | undefined) => JSX.Element; onChange: () => void; } - -export const ChangeButtonSetting = (props: IDialogSetting) => { +const _ChangeButtonSetting = (props: IDialogSetting) => { const { id, className, @@ -266,6 +262,11 @@ export const ChangeButtonSetting = (props: IDialogSetting) => { ); }; +export const ChangeButtonSetting = PatchComponent( + "ChangeButtonSetting", + _ChangeButtonSetting +) as typeof _ChangeButtonSetting; + export interface ISettingModal { heading?: React.ReactNode; headingID?: string; @@ -283,7 +284,7 @@ export interface ISettingModal { error?: string | undefined; } -export const SettingModal = (props: ISettingModal) => { +const _SettingModal = (props: ISettingModal) => { const { heading, headingID, @@ -342,6 +343,11 @@ export const SettingModal = (props: ISettingModal) => { ); }; +export const SettingModal = PatchComponent( + "SettingModal", + _SettingModal +) as typeof _SettingModal; + interface IModalSetting extends ISetting { value: T | undefined; buttonText?: string; @@ -357,7 +363,7 @@ interface IModalSetting extends ISetting { validateChange?: (v: T) => void | undefined; } -export const ModalSetting = (props: IModalSetting) => { +export const _ModalSetting = (props: IModalSetting) => { const { id, className, @@ -435,52 +441,63 @@ export const ModalSetting = (props: IModalSetting) => { ); }; +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 = (props) => { - return ( - - {...props} - renderField={(value, setValue) => ( - ) => - setValue(e.currentTarget.value) - } - /> - )} - renderValue={(value) => {value}} - /> - ); -}; +export const StringSetting: React.FC = PatchComponent( + "StringSetting", + (props) => { + return ( + + {...props} + renderField={(value, setValue) => ( + ) => + setValue(e.currentTarget.value) + } + /> + )} + renderValue={(value) => {value}} + /> + ); + } +); interface INumberSetting extends ISetting { value: number | undefined; onChange: (v: number) => void; } -export const NumberSetting: React.FC = (props) => { - return ( - - {...props} - renderField={(value, setValue) => ( - ) => - setValue(Number.parseInt(e.currentTarget.value || "0", 10)) - } - /> - )} - renderValue={(value) => {value}} - /> - ); -}; +export const NumberSetting: React.FC = PatchComponent( + "NumberSetting", + (props) => { + return ( + + {...props} + renderField={(value, setValue) => ( + ) => + setValue(Number.parseInt(e.currentTarget.value || "0", 10)) + } + /> + )} + renderValue={(value) => {value}} + /> + ); + } +); interface IStringListSetting extends ISetting { value: string[] | undefined; @@ -488,35 +505,38 @@ interface IStringListSetting extends ISetting { onChange: (v: string[]) => void; } -export const StringListSetting: React.FC = (props) => { - return ( - - {...props} - renderField={(value, setValue) => ( - - )} - renderValue={(value) => ( -
    - {value?.map((v, i) => ( - // eslint-disable-next-line react/no-array-index-key -
    {v}
    - ))} -
    - )} - /> - ); -}; +export const StringListSetting: React.FC = PatchComponent( + "StringListSetting", + (props) => { + return ( + + {...props} + renderField={(value, setValue) => ( + + )} + renderValue={(value) => ( +
    + {value?.map((v, i) => ( + // eslint-disable-next-line react/no-array-index-key +
    {v}
    + ))} +
    + )} + /> + ); + } +); interface IConstantSetting extends ISetting { value?: T; renderValue?: (v: T | undefined) => JSX.Element; } -export const ConstantSetting = (props: IConstantSetting) => { +export const _ConstantSetting = (props: IConstantSetting) => { const { id, headingID, subHeading, subHeadingID, renderValue, value } = props; const intl = useIntl(); @@ -539,3 +559,8 @@ export const ConstantSetting = (props: IConstantSetting) => {
    ); }; + +export const ConstantSetting = PatchComponent( + "ConstantSetting", + _ConstantSetting +) as typeof _ConstantSetting; diff --git a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx index 102d236f1..a4ad2b5a7 100644 --- a/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsPluginsPanel.tsx @@ -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 = ({ } }; +const PluginSettings: React.FC<{ + pluginID: string; + settings: GQL.PluginSetting[]; +}> = PatchComponent("PluginSettings", ({ pluginID, settings }) => { + const { plugins, savePluginSettings } = useSettings(); + const pluginSettings = plugins[pluginID] ?? {}; + + return ( +
    + {settings.map((setting) => ( + + savePluginSettings(pluginID, { + ...pluginSettings, + [setting.name]: v, + }) + } + /> + ))} +
    + ); +}); + 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 ?? [])} + )); @@ -208,37 +239,8 @@ export const SettingsPluginsPanel: React.FC = () => { ); } - function renderPluginSettings( - pluginID: string, - settings: GQL.PluginSetting[] - ) { - const pluginSettings = plugins[pluginID] ?? {}; - - return settings.map((setting) => ( - - savePluginSettings(pluginID, { - ...pluginSettings, - [setting.name]: v, - }) - } - /> - )); - } - return renderPlugins(); - }, [ - data?.plugins, - intl, - Toast, - changedPluginID, - plugins, - savePluginSettings, - ]); + }, [data?.plugins, intl, Toast, changedPluginID]); if (loading || configLoading) return ; diff --git a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md index 2ad2b714a..fd96cc52d 100644 --- a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md +++ b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md @@ -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` diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index f16e672aa..4967eedbf 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -681,7 +681,18 @@ declare namespace PluginApi { "SceneCard.Details": React.FC; "SceneCard.Overlays": React.FC; "SceneCard.Image": React.FC; - SceneCard: React.FC; + PluginSettings: React.FC; + Setting: React.FC; + SettingGroup: React.FC; + BooleanSetting: React.FC; + SelectSetting: React.FC; + ChangeButtonSetting: React.FC; + SettingModal: React.FC; + ModalSetting: React.FC; + StringSetting: React.FC; + NumberSetting: React.FC; + StringListSetting: React.FC; + ConstantSetting: React.FC; }; 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; diff --git a/ui/v2.5/src/pluginApi.tsx b/ui/v2.5/src/pluginApi.tsx index f3c2ee7d5..f35fb0fe6 100644 --- a/ui/v2.5/src/pluginApi.tsx +++ b/ui/v2.5/src/pluginApi.tsx @@ -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