make ImageCard patchable for plugin extensibility (#6470)

* refactor(ui): make ImageCard patchable for plugin extensibility

Refactor ImageCard component to use PatchComponent wrapper.

Changes:
- Wrap ImageCard and sub-components with PatchComponent
- Extract ImageCardPopovers, ImageCardDetails, ImageCardOverlays,
  ImageCardImage as separate patchable components

* Add documentation
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
CJ
2026-01-26 23:10:49 -06:00
committed by GitHub
parent 09044b92bf
commit 6bb22146b2
2 changed files with 95 additions and 67 deletions
+91 -67
View File
@@ -30,17 +30,9 @@ interface IImageCardProps {
onPreview?: (ev: MouseEvent) => void;
}
export const ImageCard: React.FC<IImageCardProps> = PatchComponent(
"ImageCard",
const ImageCardPopovers = PatchComponent(
"ImageCard.Popovers",
(props: IImageCardProps) => {
const file = useMemo(
() =>
props.image.visual_files.length > 0
? props.image.visual_files[0]
: undefined,
[props.image]
);
function maybeRenderTagPopoverButton() {
if (props.image.tags.length <= 0) return;
@@ -112,29 +104,65 @@ export const ImageCard: React.FC<IImageCardProps> = PatchComponent(
}
}
function maybeRenderPopoverButtonGroup() {
if (
props.image.tags.length > 0 ||
props.image.performers.length > 0 ||
props.image.o_counter ||
props.image.galleries.length > 0 ||
props.image.organized
) {
return (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);
}
if (
props.image.tags.length > 0 ||
props.image.performers.length > 0 ||
props.image.o_counter ||
props.image.galleries.length > 0 ||
props.image.organized
) {
return (
<>
<hr />
<ButtonGroup className="card-popovers">
{maybeRenderTagPopoverButton()}
{maybeRenderPerformerPopoverButton()}
{maybeRenderOCounter()}
{maybeRenderGallery()}
{maybeRenderOrganized()}
</ButtonGroup>
</>
);
}
return null;
}
);
const ImageCardDetails = PatchComponent(
"ImageCard.Details",
(props: IImageCardProps) => {
return (
<div className="image-card__details">
<span className="image-card__date">{props.image.date}</span>
<TruncatedText
className="image-card__description"
text={props.image.details}
lineCount={3}
/>
</div>
);
}
);
const ImageCardOverlays = PatchComponent(
"ImageCard.Overlays",
(props: IImageCardProps) => {
return <StudioOverlay studio={props.image.studio} />;
}
);
const ImageCardImage = PatchComponent(
"ImageCard.Image",
(props: IImageCardProps) => {
const file = useMemo(
() =>
props.image.visual_files.length > 0
? props.image.visual_files[0]
: undefined,
[props.image]
);
function isPortrait() {
const width = file?.width ? file.width : 0;
const height = file?.height ? file.height : 0;
@@ -148,6 +176,34 @@ export const ImageCard: React.FC<IImageCardProps> = PatchComponent(
const video = source.includes("preview");
const ImagePreview = video ? "video" : "img";
return (
<>
<div className={cx("image-card-preview", { portrait: isPortrait() })}>
<ImagePreview
loop={video}
autoPlay={video}
playsInline={video}
className="image-card-preview-image"
alt={props.image.title ?? ""}
src={source}
/>
{props.onPreview ? (
<div className="preview-button">
<Button onClick={props.onPreview}>
<Icon icon={faSearch} />
</Button>
</div>
) : undefined}
</div>
<RatingBanner rating={props.image.rating100} />
</>
);
}
);
export const ImageCard: React.FC<IImageCardProps> = PatchComponent(
"ImageCard",
(props: IImageCardProps) => {
return (
<GridCard
className={`image-card zoom-${props.zoomIndex}`}
@@ -155,42 +211,10 @@ export const ImageCard: React.FC<IImageCardProps> = PatchComponent(
width={props.cardWidth}
title={imageTitle(props.image)}
linkClassName="image-card-link"
image={
<>
<div
className={cx("image-card-preview", { portrait: isPortrait() })}
>
<ImagePreview
loop={video}
autoPlay={video}
playsInline={video}
className="image-card-preview-image"
alt={props.image.title ?? ""}
src={source}
/>
{props.onPreview ? (
<div className="preview-button">
<Button onClick={props.onPreview}>
<Icon icon={faSearch} />
</Button>
</div>
) : undefined}
</div>
<RatingBanner rating={props.image.rating100} />
</>
}
details={
<div className="image-card__details">
<span className="image-card__date">{props.image.date}</span>
<TruncatedText
className="image-card__description"
text={props.image.details}
lineCount={3}
/>
</div>
}
overlays={<StudioOverlay studio={props.image.studio} />}
popovers={maybeRenderPopoverButtonGroup()}
image={<ImageCardImage {...props} />}
details={<ImageCardDetails {...props} />}
overlays={<ImageCardOverlays {...props} />}
popovers={<ImageCardPopovers {...props} />}
selected={props.selected}
selecting={props.selecting}
onSelectedChanged={props.onSelectedChanged}
@@ -252,6 +252,10 @@ Returns `void`.
- `HoverPopover`
- `Icon`
- `ImageCard`
- `ImageCard.Details`
- `ImageCard.Image`
- `ImageCard.Overlays`
- `ImageCard.Popovers`
- `ImageDetailPanel`
- `ImageGridCard`
- `ImageInput`