From c7bfdfd3cdf7a03a569ea5fe78abc00a71310283 Mon Sep 17 00:00:00 2001 From: KennyG Date: Tue, 21 Apr 2026 16:03:02 -0400 Subject: [PATCH] Enhance performer previews with delta rows and new scraped performer component - Added delta row functionality to display differences between local and remote performer data in MatchedPerformerPreview. - Introduced ScrapedPerformerPreview component for displaying scraped performer details with hover functionality. - Updated PerformerResult to integrate ScrapedPerformerPreview and pass necessary props for delta rows and warnings. - Enhanced styling for performer popovers to improve user experience. --- .../Performers/PerformerPopover.tsx | 3 + .../Tagger/scenes/MatchedPerformerPreview.tsx | 66 +++++++++- .../Tagger/scenes/PerformerResult.tsx | 121 ++++++++++++++++-- .../Tagger/scenes/ScrapedPerformerPreview.tsx | 70 ++++++++++ ui/v2.5/src/components/Tagger/styles.scss | 33 +++++ 5 files changed, 276 insertions(+), 17 deletions(-) create mode 100644 ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerPreview.tsx diff --git a/ui/v2.5/src/components/Performers/PerformerPopover.tsx b/ui/v2.5/src/components/Performers/PerformerPopover.tsx index 52f825a50..5c2815917 100644 --- a/ui/v2.5/src/components/Performers/PerformerPopover.tsx +++ b/ui/v2.5/src/components/Performers/PerformerPopover.tsx @@ -43,6 +43,7 @@ interface IPeroformerPopoverProps { placement?: Placement; target?: React.RefObject; cardClassName?: string; + triggerClassName?: string; } export const PerformerPopover: React.FC = ({ @@ -52,6 +53,7 @@ export const PerformerPopover: React.FC = ({ placement = "top", target, cardClassName, + triggerClassName, }) => { const { configuration: config } = useConfigurationContext(); @@ -63,6 +65,7 @@ export const PerformerPopover: React.FC = ({ return ( ; children?: ReactNode; } export const MatchedPerformerPreview = ({ performerID, - placement = "right", + performer, + placement = "bottom", + deltaRows = [], + warningStashID, children, }: IMatchedPerformerPreviewProps) => { - if (!performerID) { + const { configuration: config } = useConfigurationContext(); + const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true; + const warningEndpointName = warningStashID + ? config?.general.stashBoxes.find((sb) => sb.endpoint === warningStashID.endpoint) + ?.name ?? warningStashID.endpoint + : null; + + if (!performerID || !performer || !showPerformerCardOnHover) { return <>{children}; } return ( - + + {(warningStashID || deltaRows.length > 0) && ( +
+ {warningStashID && ( +
+ + + {warningEndpointName} + + +
+ )} + {deltaRows.length > 0 && ( +
+ {deltaRows.map((row) => ( +
+ {row.label}: {row.value} +
+ ))} +
+ )} +
+ )} + + } > {children} -
+
); }; diff --git a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index f3c67e964..a864617f2 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -13,6 +13,88 @@ import { ExternalLink } from "src/components/Shared/ExternalLink"; import { Link } from "react-router-dom"; import { LinkButton } from "../LinkButton"; import { MatchedPerformerPreview } from "./MatchedPerformerPreview"; +import { ScrapedPerformerPreview } from "./ScrapedPerformerPreview"; + +interface IPerformerDeltaRow { + label: string; + value: string; +} + +const normalizeValue = (value: unknown) => + String(value ?? "") + .trim() + .toLowerCase(); + +const toStringOrNull = (value: unknown) => { + if (value === null || value === undefined) return null; + const text = String(value).trim(); + return text.length > 0 ? text : null; +}; + +const pushDeltaIfDifferent = ( + rows: IPerformerDeltaRow[], + label: string, + remoteValue: unknown, + localValue: unknown +) => { + const remoteText = toStringOrNull(remoteValue); + if (!remoteText) return; + + if (normalizeValue(remoteText) === normalizeValue(localValue)) return; + rows.push({ label, value: remoteText }); +}; + +const buildPerformerDeltaRows = ( + remote: GQL.ScrapedPerformer, + local: Performer +): IPerformerDeltaRow[] => { + const rows: IPerformerDeltaRow[] = []; + + pushDeltaIfDifferent(rows, "Birthdate", remote.birthdate, local.birthdate); + pushDeltaIfDifferent(rows, "Death Date", remote.death_date, local.death_date); + pushDeltaIfDifferent(rows, "Ethnicity", remote.ethnicity, local.ethnicity); + pushDeltaIfDifferent(rows, "Hair Color", remote.hair_color, local.hair_color); + pushDeltaIfDifferent(rows, "Eye Color", remote.eye_color, local.eye_color); + pushDeltaIfDifferent(rows, "Height", remote.height, local.height_cm); + pushDeltaIfDifferent(rows, "Weight", remote.weight, local.weight); + pushDeltaIfDifferent( + rows, + "Penis Length", + remote.penis_length, + local.penis_length + ); + pushDeltaIfDifferent(rows, "Circumcised", remote.circumcised, local.circumcised); + pushDeltaIfDifferent( + rows, + "Measurements", + remote.measurements, + local.measurements + ); + pushDeltaIfDifferent(rows, "Fake Tits", remote.fake_tits, local.fake_tits); + pushDeltaIfDifferent(rows, "Tattoos", remote.tattoos, local.tattoos); + pushDeltaIfDifferent(rows, "Piercings", remote.piercings, local.piercings); + pushDeltaIfDifferent(rows, "Career Start", remote.career_start, local.career_start); + pushDeltaIfDifferent(rows, "Career End", remote.career_end, local.career_end); + + const remoteAliasesCount = remote.aliases + ? remote.aliases + .split(",") + .map((a) => a.trim()) + .filter(Boolean).length + : 0; + const localAliasesCount = local.alias_list?.length ?? 0; + if (remoteAliasesCount > 0 && remoteAliasesCount !== localAliasesCount) { + rows.push({ label: "Aliases", value: String(remoteAliasesCount) }); + } + + const remoteUrlsCount = remote.urls?.length ?? 0; + const localUrlsCount = local.urls?.length ?? 0; + if (remoteUrlsCount > 0 && remoteUrlsCount !== localUrlsCount) { + rows.push({ label: "URLs", value: String(remoteUrlsCount) }); + } + + return rows; +}; const PerformerLink: React.FC<{ performer: GQL.ScrapedPerformer | Performer; @@ -74,7 +156,6 @@ const PerformerResult: React.FC = ({ stashID.endpoint === endpoint && stashID.stash_id === performer.remote_site_id ); - const [selectedPerformer, setSelectedPerformer] = useState(); const stashboxPerformerPrefix = endpoint @@ -116,10 +197,12 @@ const PerformerResult: React.FC = ({
: - + + +
@@ -148,6 +231,17 @@ const PerformerResult: React.FC = ({ } const selectedSource = !selectedID ? "skip" : "existing"; + const selectedPerformerConflictStashID = + endpoint && performer.remote_site_id && selectedPerformer + ? selectedPerformer.stash_ids.find( + (stashID) => + stashID.endpoint === endpoint && + stashID.stash_id !== performer.remote_site_id + ) + : undefined; + const selectedPerformerDeltaRows = selectedPerformer + ? buildPerformerDeltaRows(performer, selectedPerformer) + : []; const safeBuildPerformerScraperLink = (id: string | null | undefined) => { return stashboxPerformerPrefix && id @@ -160,10 +254,12 @@ const PerformerResult: React.FC = ({
: - + + +
@@ -176,7 +272,12 @@ const PerformerResult: React.FC = ({ > - + + ({ + id: + performer.stored_id ?? + performer.remote_site_id ?? + `scraped-${performer.name?.replace(/\s+/g, "-").toLowerCase() ?? "performer"}`, + name: performer.name ?? "Unknown performer", + disambiguation: performer.disambiguation ?? null, + gender: performer.gender ?? null, + birthdate: performer.birthdate ?? null, + death_date: performer.death_date ?? null, + country: performer.country ?? null, + image_path: performer.images?.[0] ?? performer.image_path ?? null, + tags: [], + stash_ids: [], + favorite: false, + scene_count: null, + image_count: null, + gallery_count: null, + group_count: null, + o_counter: null, + rating100: null, + urls: performer.urls ?? [], + }) as GQL.PerformerDataFragment; + +const ScrapedPerformerCard = ({ performer }: { performer: GQL.ScrapedPerformer }) => { + return ( +
+ +
+ ); +}; + +export const ScrapedPerformerPreview = ({ + performer, + placement = "bottom", + children, +}: IScrapedPerformerPreviewProps) => { + const { configuration: config } = useConfigurationContext(); + const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true; + + if (!showPerformerCardOnHover) { + return <>{children}; + } + + return ( + } + > + {children} + + ); +}; diff --git a/ui/v2.5/src/components/Tagger/styles.scss b/ui/v2.5/src/components/Tagger/styles.scss index 006dba876..c76c20358 100644 --- a/ui/v2.5/src/components/Tagger/styles.scss +++ b/ui/v2.5/src/components/Tagger/styles.scss @@ -105,6 +105,39 @@ } } +.tagger-matched-performer-popover-extra { + border-top: 1px solid rgba(255, 255, 255, 0.12); + margin-top: 0.4rem; + padding-top: 0.5rem; +} + +.tagger-scraped-performer-popover { + .card { + width: 200px; + } + + .performer-card-image { + max-height: 300px; + } +} + +.tagger-performer-delta-rows { + font-size: 0.875rem; + line-height: 1.3; + text-align: left; +} + +.tagger-performer-stashid-warning { + text-align: left; + + .tagger-performer-stashid-warning-chip { + background-color: $danger; + border-radius: 0.25rem; + color: $white; + min-width: auto; + } +} + .selected-result { background-color: hsl(204, 20%, 30%); border-radius: 3px;