diff --git a/ui/v2.5/src/components/Tagger/scenes/MatchedPerformerPreview.tsx b/ui/v2.5/src/components/Tagger/scenes/MatchedPerformerPreview.tsx deleted file mode 100644 index afe74ca90..000000000 --- a/ui/v2.5/src/components/Tagger/scenes/MatchedPerformerPreview.tsx +++ /dev/null @@ -1,256 +0,0 @@ -import { ReactNode, useState } from "react"; -import { Placement } from "react-bootstrap/esm/Overlay"; -import { IntlShape, useIntl } from "react-intl"; -import * as GQL from "src/core/generated-graphql"; -import { useConfigurationContext } from "src/hooks/Config"; -import { TaggerPerformerPopover } from "./TaggerPerformerPopover"; - -interface IPerformerDeltaRow { - label: string; - value: string; -} - -interface IMatchedPerformerPreviewProps { - performerID?: string | null; - scrapedPerformer: GQL.ScrapedPerformer; - endpoint?: string; - placement?: Placement; - children?: ReactNode; -} - -const normalizeValue = (value: unknown) => - (() => { - const text = String(value ?? "").trim(); - if (!text) return ""; - - const isNumericLike = /^[-+]?(?:\d+\.?\d*|\.\d+)$/.test(text); - if (isNumericLike) { - const numeric = Number(text); - if (!Number.isNaN(numeric)) { - return String(numeric); - } - } - - return text.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: GQL.PerformerDataFragment, - intl: IntlShape -): IPerformerDeltaRow[] => { - const rows: IPerformerDeltaRow[] = []; - - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "birthdate" }), - remote.birthdate, - local.birthdate - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "death_date" }), - remote.death_date, - local.death_date - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "ethnicity" }), - remote.ethnicity, - local.ethnicity - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "hair_color" }), - remote.hair_color, - local.hair_color - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "eye_color" }), - remote.eye_color, - local.eye_color - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "height" }), - remote.height, - local.height_cm - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "weight" }), - remote.weight, - local.weight - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "penis_length" }), - remote.penis_length, - local.penis_length - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "circumcised" }), - remote.circumcised, - local.circumcised - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "measurements" }), - remote.measurements, - local.measurements - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "fake_tits" }), - remote.fake_tits, - local.fake_tits - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "tattoos" }), - remote.tattoos, - local.tattoos - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "piercings" }), - remote.piercings, - local.piercings - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "career_start" }), - remote.career_start, - local.career_start - ); - pushDeltaIfDifferent( - rows, - intl.formatMessage({ id: "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 > localAliasesCount) { - rows.push({ - label: intl.formatMessage({ id: "aliases" }), - value: String(remoteAliasesCount), - }); - } - - const remoteUrlsCount = remote.urls?.length ?? 0; - const localUrlsCount = local.urls?.length ?? 0; - if (remoteUrlsCount > localUrlsCount) { - rows.push({ - label: intl.formatMessage({ id: "urls" }), - value: String(remoteUrlsCount), - }); - } - - return rows; -}; - -export const MatchedPerformerPreview = ({ - performerID, - scrapedPerformer, - endpoint, - placement = "bottom", - children, -}: IMatchedPerformerPreviewProps) => { - const intl = useIntl(); - const { configuration: config } = useConfigurationContext(); - const showPerformerCardOnHover = config?.ui.showTagCardOnHover ?? true; - const [isOpened, setIsOpened] = useState(false); - const { data: selectedPerformerData } = GQL.useFindPerformerQuery({ - variables: { id: performerID ?? "" }, - skip: !performerID || !isOpened, - }); - const performer = selectedPerformerData?.findPerformer; - const warningStashID = - endpoint && scrapedPerformer.remote_site_id && performer - ? performer.stash_ids.find( - (stashID) => - stashID.endpoint === endpoint && - stashID.stash_id !== scrapedPerformer.remote_site_id - ) - : undefined; - const deltaRows = performer - ? buildPerformerDeltaRows(scrapedPerformer, performer, intl) - : []; - const warningEndpointName = warningStashID - ? config?.general.stashBoxes.find( - (sb) => sb.endpoint === warningStashID.endpoint - )?.name ?? warningStashID.endpoint - : null; - - if (!performerID || !showPerformerCardOnHover) { - return <>{children}; - } - - return ( - 0 ? ( -
- {warningStashID && ( -
- - - {warningEndpointName} - - -
- )} - {deltaRows.length > 0 && ( -
- {deltaRows.map((row) => ( -
- {row.label}: {row.value} -
- ))} -
- )} -
- ) : null - } - triggerClassName="d-inline-block" - placement={placement} - onOpen={() => setIsOpened(true)} - onClose={() => setIsOpened(false)} - > - {children} -
- ); -}; diff --git a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx index f71cdae3f..e16b98a88 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -12,7 +12,6 @@ import { getStashboxBase } from "src/utils/stashbox"; import { ExternalLink } from "src/components/Shared/ExternalLink"; import { Link } from "react-router-dom"; import { LinkButton } from "../LinkButton"; -import { MatchedPerformerPreview } from "./MatchedPerformerPreview"; import { TaggerPerformerPopover } from "./TaggerPerformerPopover"; const PerformerLink: React.FC<{ @@ -186,10 +185,11 @@ const PerformerResult: React.FC = ({ > - = ({ isClearable={false} ageFromDate={ageFromDate} /> - + {endpoint && onLink && ( )} diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx index 0ecb17547..4a69f7a00 100644 --- a/ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx @@ -1,16 +1,183 @@ -import React from "react"; +import React, { useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { Placement } from "react-bootstrap/esm/Overlay"; +import { IntlShape, useIntl } from "react-intl"; import { PerformerPopover } from "src/components/Performers/PerformerPopover"; import { PerformerCard } from "src/components/Performers/PerformerCard"; +import { useConfigurationContext } from "src/hooks/Config"; import { ScrapedPerformerCard } from "./ScrapedPerformerCard"; +interface IPerformerDeltaRow { + label: string; + value: string; +} + +const normalizeValue = (value: unknown) => + (() => { + const text = String(value ?? "").trim(); + if (!text) return ""; + + const isNumericLike = /^[-+]?(?:\d+\.?\d*|\.\d+)$/.test(text); + if (isNumericLike) { + const numeric = Number(text); + if (!Number.isNaN(numeric)) { + return String(numeric); + } + } + + return text.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: GQL.PerformerDataFragment, + intl: IntlShape +): IPerformerDeltaRow[] => { + const rows: IPerformerDeltaRow[] = []; + + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "birthdate" }), + remote.birthdate, + local.birthdate + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "death_date" }), + remote.death_date, + local.death_date + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "ethnicity" }), + remote.ethnicity, + local.ethnicity + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "hair_color" }), + remote.hair_color, + local.hair_color + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "eye_color" }), + remote.eye_color, + local.eye_color + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "height" }), + remote.height, + local.height_cm + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "weight" }), + remote.weight, + local.weight + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "penis_length" }), + remote.penis_length, + local.penis_length + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "circumcised" }), + remote.circumcised, + local.circumcised + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "measurements" }), + remote.measurements, + local.measurements + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "fake_tits" }), + remote.fake_tits, + local.fake_tits + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "tattoos" }), + remote.tattoos, + local.tattoos + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "piercings" }), + remote.piercings, + local.piercings + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "career_start" }), + remote.career_start, + local.career_start + ); + pushDeltaIfDifferent( + rows, + intl.formatMessage({ id: "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 > localAliasesCount) { + rows.push({ + label: intl.formatMessage({ id: "aliases" }), + value: String(remoteAliasesCount), + }); + } + + const remoteUrlsCount = remote.urls?.length ?? 0; + const localUrlsCount = local.urls?.length ?? 0; + if (remoteUrlsCount > localUrlsCount) { + rows.push({ + label: intl.formatMessage({ id: "urls" }), + value: String(remoteUrlsCount), + }); + } + + return rows; +}; + interface ITaggerPerformerPopoverProps { performer?: GQL.PerformerDataFragment; performerID?: string; scrapedPerformer?: GQL.ScrapedPerformer; endpoint?: string; cardExtras?: React.ReactNode; + includeMatchExtras?: boolean; placement?: Placement; triggerClassName?: string; onOpen?: () => void; @@ -25,15 +192,26 @@ export const TaggerPerformerPopover: React.FC< scrapedPerformer, endpoint, cardExtras, + includeMatchExtras = false, placement = "bottom", triggerClassName = "d-inline-block", onOpen, onClose, children, }) => { - const cardContent = performer ? ( + const intl = useIntl(); + const { configuration: config } = useConfigurationContext(); + const [isOpened, setIsOpened] = useState(false); + + const { data: selectedPerformerData } = GQL.useFindPerformerQuery({ + variables: { id: performerID ?? "" }, + skip: !performerID || !!performer || !isOpened, + }); + const localPerformer = performer ?? selectedPerformerData?.findPerformer; + + const cardContent = localPerformer ? (
- +
) : scrapedPerformer ? (
@@ -45,17 +223,80 @@ export const TaggerPerformerPopover: React.FC<
) : undefined; + const warningStashID = + includeMatchExtras && + endpoint && + scrapedPerformer?.remote_site_id && + localPerformer + ? localPerformer.stash_ids.find( + (stashID) => + stashID.endpoint === endpoint && + stashID.stash_id !== scrapedPerformer.remote_site_id + ) + : undefined; + + const deltaRows = + includeMatchExtras && scrapedPerformer && localPerformer + ? buildPerformerDeltaRows(scrapedPerformer, localPerformer, intl) + : []; + + const warningEndpointName = warningStashID + ? config?.general.stashBoxes.find( + (sb) => sb.endpoint === warningStashID.endpoint + )?.name ?? warningStashID.endpoint + : null; + + const matchExtras = + warningStashID || deltaRows.length > 0 ? ( +
+ {warningStashID && ( +
+ + + {warningEndpointName} + + +
+ )} + {deltaRows.length > 0 && ( +
+ {deltaRows.map((row) => ( +
+ {row.label}: {row.value} +
+ ))} +
+ )} +
+ ) : null; + return ( + {matchExtras} + {cardExtras} + + ) : null + } placement={placement} enterDelay={1000} leaveDelay={500} triggerClassName={triggerClassName} - onOpen={onOpen} - onClose={onClose} + onOpen={() => { + setIsOpened(true); + onOpen?.(); + }} + onClose={() => { + setIsOpened(false); + onClose?.(); + }} > {children}