diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx index f1de9a5f1..03530c52e 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/Performer.tsx @@ -46,6 +46,7 @@ import { AliasList } from "src/components/Shared/DetailsPage/AliasList"; import { HeaderImage } from "src/components/Shared/DetailsPage/HeaderImage"; import { LightboxLink } from "src/hooks/Lightbox/LightboxLink"; import { PatchComponent } from "src/patch"; +import { ILightboxImage } from "src/hooks/Lightbox/types"; interface IProps { performer: GQL.PerformerDataFragment; @@ -201,6 +202,34 @@ const PerformerTabs: React.FC<{ ); }; +interface IPerformerHeaderImageProps { + activeImage: string | null | undefined; + collapsed: boolean; + encodingImage: boolean; + lightboxImages: ILightboxImage[]; + performer: GQL.PerformerDataFragment; +} + +const PerformerHeaderImage: React.FC = + PatchComponent( + "PerformerHeaderImage", + ({ encodingImage, activeImage, lightboxImages, performer }) => { + return ( + + {!!activeImage && ( + + + + )} + + ); + } + ); + const PerformerPage: React.FC = PatchComponent( "PerformerPage", ({ performer, tabKey }) => { @@ -364,18 +393,13 @@ const PerformerPage: React.FC = PatchComponent( show={enableBackgroundImage && !isEditing} />
- - {!!activeImage && ( - - - - )} - - +
void; isNew?: boolean; error?: string; -}> = ({ field, value, onChange, isNew = false, error }) => { - const intl = useIntl(); - const [currentField, setCurrentField] = useState(field); - const [currentValue, setCurrentValue] = useState(value as string); +}> = PatchComponent( + "CustomFieldInput", + ({ field, value, onChange, isNew = false, error }) => { + const intl = useIntl(); + const [currentField, setCurrentField] = useState(field); + const [currentValue, setCurrentValue] = useState(value as string); - const fieldRef = useRef(null); - const valueRef = useRef(null); + const fieldRef = useRef(null); + const valueRef = useRef(null); - useEffect(() => { - setCurrentField(field); - setCurrentValue(value as string); - }, [field, value]); + useEffect(() => { + setCurrentField(field); + setCurrentValue(value as string); + }, [field, value]); - function onBlur() { - onChange(currentField, convertCustomValue(currentValue)); - } + function onBlur() { + onChange(currentField, convertCustomValue(currentValue)); + } - function onDelete() { - onChange("", ""); - } + function onDelete() { + onChange("", ""); + } - return ( - - - - {isNew ? ( - <> + return ( + + + + {isNew ? ( + <> + + setCurrentField(event.currentTarget.value) + } + onBlur={onBlur} + /> + + ) : ( + {currentField} + )} + + + setCurrentField(event.currentTarget.value)} + value={(currentValue as string) ?? ""} + placeholder={currentField} + onChange={(event) => setCurrentValue(event.currentTarget.value)} onBlur={onBlur} /> - - ) : ( - {currentField} - )} - - - - setCurrentValue(event.currentTarget.value)} - onBlur={onBlur} - /> - - {!isNew && ( - - )} - - - - - {error} - - ); -}; + + {!isNew && ( + + )} + + + + + {error} + + ); + } +); interface ICustomField { field: string; diff --git a/ui/v2.5/src/components/Shared/DetailImage.tsx b/ui/v2.5/src/components/Shared/DetailImage.tsx index 357e5eee0..1d9174136 100644 --- a/ui/v2.5/src/components/Shared/DetailImage.tsx +++ b/ui/v2.5/src/components/Shared/DetailImage.tsx @@ -1,4 +1,5 @@ import { useLayoutEffect, useRef } from "react"; +import { PatchComponent } from "src/patch"; import { remToPx } from "src/utils/units"; const DEFAULT_WIDTH = Math.round(remToPx(30)); @@ -6,34 +7,37 @@ const DEFAULT_WIDTH = Math.round(remToPx(30)); // Props used by the element type IDetailImageProps = JSX.IntrinsicElements["img"]; -export const DetailImage = (props: IDetailImageProps) => { - const imgRef = useRef(null); +export const DetailImage = PatchComponent( + "DetailImage", + (props: IDetailImageProps) => { + const imgRef = useRef(null); - function fixWidth() { - const img = imgRef.current; - if (!img) return; + function fixWidth() { + const img = imgRef.current; + if (!img) return; - // prevent SVG's w/o intrinsic size from rendering as 0x0 - if (img.naturalWidth === 0) { - // If the naturalWidth is zero, it means the image either hasn't loaded yet - // or we're on Firefox and it is an SVG w/o an intrinsic size. - // So set the width to our fallback width. - img.setAttribute("width", String(DEFAULT_WIDTH)); - } else { - // If we have a `naturalWidth`, this could either be the actual intrinsic width - // of the image, or the image is an SVG w/o an intrinsic size and we're on Chrome or Safari, - // which seem to return a size calculated in some browser-specific way. - // Worse yet, once rendered, Safari will then return the value of `img.width` as `img.naturalWidth`, - // so we need to clone the image to disconnect it from the DOM, and then get the `naturalWidth` of the clone, - // in order to always return the same `naturalWidth` for a given src. - const i = img.cloneNode() as HTMLImageElement; - img.setAttribute("width", String(i.naturalWidth || DEFAULT_WIDTH)); + // prevent SVG's w/o intrinsic size from rendering as 0x0 + if (img.naturalWidth === 0) { + // If the naturalWidth is zero, it means the image either hasn't loaded yet + // or we're on Firefox and it is an SVG w/o an intrinsic size. + // So set the width to our fallback width. + img.setAttribute("width", String(DEFAULT_WIDTH)); + } else { + // If we have a `naturalWidth`, this could either be the actual intrinsic width + // of the image, or the image is an SVG w/o an intrinsic size and we're on Chrome or Safari, + // which seem to return a size calculated in some browser-specific way. + // Worse yet, once rendered, Safari will then return the value of `img.width` as `img.naturalWidth`, + // so we need to clone the image to disconnect it from the DOM, and then get the `naturalWidth` of the clone, + // in order to always return the same `naturalWidth` for a given src. + const i = img.cloneNode() as HTMLImageElement; + img.setAttribute("width", String(i.naturalWidth || DEFAULT_WIDTH)); + } } + + useLayoutEffect(() => { + fixWidth(); + }, [props.src]); + + return fixWidth()} {...props} />; } - - useLayoutEffect(() => { - fixWidth(); - }, [props.src]); - - return fixWidth()} {...props} />; -}; +); diff --git a/ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx b/ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx index 057115308..02b405aa7 100644 --- a/ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx +++ b/ui/v2.5/src/components/Shared/DetailsPage/HeaderImage.tsx @@ -1,12 +1,13 @@ import { PropsWithChildren } from "react"; import { LoadingIndicator } from "../LoadingIndicator"; import { FormattedMessage } from "react-intl"; +import { PatchComponent } from "src/patch"; export const HeaderImage: React.FC< PropsWithChildren<{ encodingImage: boolean; }> -> = ({ encodingImage, children }) => { +> = PatchComponent("HeaderImage", ({ encodingImage, children }) => { return (
{encodingImage ? ( @@ -18,4 +19,4 @@ export const HeaderImage: React.FC< )}
); -}; +}); diff --git a/ui/v2.5/src/components/Shared/ImageInput.tsx b/ui/v2.5/src/components/Shared/ImageInput.tsx index 05b5eb264..94a83f952 100644 --- a/ui/v2.5/src/components/Shared/ImageInput.tsx +++ b/ui/v2.5/src/components/Shared/ImageInput.tsx @@ -11,6 +11,7 @@ import { useIntl } from "react-intl"; import { ModalComponent } from "./Modal"; import { Icon } from "./Icon"; import { faFile, faLink } from "@fortawesome/free-solid-svg-icons"; +import { PatchComponent } from "src/patch"; interface IImageInput { isEditing: boolean; @@ -24,122 +25,119 @@ function acceptExtensions(acceptSVG: boolean = false) { return `.jpg,.jpeg,.png,.webp,.gif${acceptSVG ? ",.svg" : ""}`; } -export const ImageInput: React.FC = ({ - isEditing, - text, - onImageChange, - onImageURL, - acceptSVG = false, -}) => { - const [isShowDialog, setIsShowDialog] = useState(false); - const [url, setURL] = useState(""); - const intl = useIntl(); +export const ImageInput: React.FC = PatchComponent( + "ImageInput", + ({ isEditing, text, onImageChange, onImageURL, acceptSVG = false }) => { + const [isShowDialog, setIsShowDialog] = useState(false); + const [url, setURL] = useState(""); + const intl = useIntl(); - if (!isEditing) return
; + if (!isEditing) return
; - if (!onImageURL) { - // just return the file input - return ( - - - - - ); - } - - function showDialog() { - setURL(""); - setIsShowDialog(true); - } - - function onConfirmURL() { if (!onImageURL) { - return; + // just return the file input + return ( + + + + + ); } - setIsShowDialog(false); - onImageURL(url); - } + function showDialog() { + setURL(""); + setIsShowDialog(true); + } + + function onConfirmURL() { + if (!onImageURL) { + return; + } + + setIsShowDialog(false); + onImageURL(url); + } + + function renderDialog() { + return ( + setIsShowDialog(false)} + header={intl.formatMessage({ id: "dialogs.set_image_url_title" })} + accept={{ + onClick: onConfirmURL, + text: intl.formatMessage({ id: "actions.confirm" }), + }} + > +
+ + + {intl.formatMessage({ id: "url" })} + + + ) => + setURL(event.currentTarget.value) + } + value={url} + placeholder={intl.formatMessage({ id: "url" })} + /> + + +
+
+ ); + } + + const popover = ( + + + <> +
+ + + + +
+
+ +
+ +
+
+ ); - function renderDialog() { return ( - setIsShowDialog(false)} - header={intl.formatMessage({ id: "dialogs.set_image_url_title" })} - accept={{ - onClick: onConfirmURL, - text: intl.formatMessage({ id: "actions.confirm" }), - }} - > -
- - - {intl.formatMessage({ id: "url" })} - - - ) => - setURL(event.currentTarget.value) - } - value={url} - placeholder={intl.formatMessage({ id: "url" })} - /> - - -
-
+ <> + {renderDialog()} + + + + ); } - - const popover = ( - - - <> -
- - - - -
-
- -
- -
-
- ); - - return ( - <> - {renderDialog()} - - - - - ); -}; +); diff --git a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md index 529451a25..d5a91b567 100644 --- a/ui/v2.5/src/docs/en/Manual/UIPluginApi.md +++ b/ui/v2.5/src/docs/en/Manual/UIPluginApi.md @@ -149,7 +149,9 @@ Returns `void`. - `CompressedPerformerDetailsPanel` - `ConstantSetting` - `CountrySelect` +- `CustomFieldInput` - `DateInput` +- `DetailImage` - `ExternalLinkButtons` - `ExternalLinksButton` - `FolderSelect` @@ -162,9 +164,12 @@ Returns `void`. - `GalleryIDSelect` - `GallerySelect` - `GallerySelect.sort` +- `HeaderImage` - `HoverPopover` - `Icon` - `ImageDetailPanel` +- `ImageInput` +- `LightboxLink` - `LoadingIndicator` - `ModalSetting` - `GroupIDSelect` @@ -186,6 +191,7 @@ Returns `void`. - `PerformerSelect.sort` - `PerformerGalleriesPanel` - `PerformerGroupsPanel` +- `PerformerHeaderImage` - `PerformerImagesPanel` - `PerformerScenesPanel` - `PluginRoutes` diff --git a/ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx b/ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx index 585b05067..815cab61a 100644 --- a/ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx +++ b/ui/v2.5/src/hooks/Lightbox/LightboxLink.tsx @@ -2,10 +2,11 @@ import { PropsWithChildren } from "react"; import { useLightbox } from "./hooks"; import { ILightboxImage } from "./types"; import { Button } from "react-bootstrap"; +import { PatchComponent } from "src/patch"; export const LightboxLink: React.FC< PropsWithChildren<{ images?: ILightboxImage[] | undefined; index?: number }> -> = ({ images, index, children }) => { +> = PatchComponent("LightboxLink", ({ images, index, children }) => { const showLightbox = useLightbox(); if (!images || images.length === 0) { @@ -20,4 +21,4 @@ export const LightboxLink: React.FC< {children} ); -}; +}); diff --git a/ui/v2.5/src/pluginApi.d.ts b/ui/v2.5/src/pluginApi.d.ts index 3cb5e9400..01f6b754c 100644 --- a/ui/v2.5/src/pluginApi.d.ts +++ b/ui/v2.5/src/pluginApi.d.ts @@ -694,12 +694,18 @@ declare namespace PluginApi { PerformerAppearsWithPanel: React.FC; PerformerGalleriesPanel: React.FC; PerformerGroupsPanel: React.FC; + PerformerHeaderImage: React.FC; PerformerScenesPanel: React.FC; PerformerImagesPanel: React.FC; TabTitleCounter: React.FC; PerformerCard: React.FC; ExternalLinkButtons: React.FC; ExternalLinksButton: React.FC; + CustomFieldInput: React.FC; + ImageInput: React.FC; + DetailImage: React.FC; + HeaderImage: React.FC; + LightboxLink: React.FC; "PerformerCard.Popovers": React.FC; "PerformerCard.Details": React.FC; "PerformerCard.Overlays": React.FC;