mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
Simplify and balance wrapper for performer popover
- Removed MatchedPerformerPreview component and integrated its functionality into TaggerPerformerPopover. - Updated PerformerResult to utilize TaggerPerformerPopover for displaying performer details. - Enhanced TaggerPerformerPopover to include delta row calculations for differences between local and scraped performer data. - Improved code organization and maintainability by consolidating related logic into the popover component.
This commit is contained in:
parent
6f655d2bda
commit
d8cbabf383
3 changed files with 250 additions and 265 deletions
|
|
@ -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 (
|
||||
<TaggerPerformerPopover
|
||||
performer={performer ?? undefined}
|
||||
performerID={performerID ?? undefined}
|
||||
cardExtras={
|
||||
warningStashID || deltaRows.length > 0 ? (
|
||||
<div className="tagger-matched-performer-popover-extra">
|
||||
{warningStashID && (
|
||||
<div className="tagger-performer-stashid-warning">
|
||||
<span className="stash-id-pill">
|
||||
<span
|
||||
className="tagger-performer-stashid-warning-chip"
|
||||
title={warningStashID.stash_id}
|
||||
>
|
||||
{warningEndpointName}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{deltaRows.length > 0 && (
|
||||
<div className="tagger-performer-delta-rows mt-2">
|
||||
{deltaRows.map((row) => (
|
||||
<div key={row.label}>
|
||||
<span>{row.label}:</span> <span>{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null
|
||||
}
|
||||
triggerClassName="d-inline-block"
|
||||
placement={placement}
|
||||
onOpen={() => setIsOpened(true)}
|
||||
onClose={() => setIsOpened(false)}
|
||||
>
|
||||
{children}
|
||||
</TaggerPerformerPopover>
|
||||
);
|
||||
};
|
||||
|
|
@ -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<IPerformerResultProps> = ({
|
|||
>
|
||||
<FormattedMessage id="actions.skip" />
|
||||
</Button>
|
||||
<MatchedPerformerPreview
|
||||
<TaggerPerformerPopover
|
||||
performerID={selectedPerformer?.id}
|
||||
scrapedPerformer={performer}
|
||||
endpoint={endpoint}
|
||||
includeMatchExtras
|
||||
>
|
||||
<PerformerSelect
|
||||
values={selectedPerformer ? [selectedPerformer] : []}
|
||||
|
|
@ -198,7 +198,7 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||
isClearable={false}
|
||||
ageFromDate={ageFromDate}
|
||||
/>
|
||||
</MatchedPerformerPreview>
|
||||
</TaggerPerformerPopover>
|
||||
{endpoint && onLink && (
|
||||
<LinkButton disabled={selectedID === undefined} onLink={onLink} />
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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 ? (
|
||||
<div className="tag-popover-card tagger-performer-popover-card">
|
||||
<PerformerCard performer={performer} zoomIndex={0} />
|
||||
<PerformerCard performer={localPerformer} zoomIndex={0} />
|
||||
</div>
|
||||
) : scrapedPerformer ? (
|
||||
<div className="tag-popover-card tagger-performer-popover-card">
|
||||
|
|
@ -45,17 +223,80 @@ export const TaggerPerformerPopover: React.FC<
|
|||
</div>
|
||||
) : 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 ? (
|
||||
<div className="tagger-matched-performer-popover-extra">
|
||||
{warningStashID && (
|
||||
<div className="tagger-performer-stashid-warning">
|
||||
<span className="stash-id-pill">
|
||||
<span
|
||||
className="tagger-performer-stashid-warning-chip"
|
||||
title={warningStashID.stash_id}
|
||||
>
|
||||
{warningEndpointName}
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{deltaRows.length > 0 && (
|
||||
<div className="tagger-performer-delta-rows mt-2">
|
||||
{deltaRows.map((row) => (
|
||||
<div key={row.label}>
|
||||
<span>{row.label}:</span> <span>{row.value}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null;
|
||||
|
||||
return (
|
||||
<PerformerPopover
|
||||
id={cardContent ? undefined : performerID}
|
||||
cardContent={cardContent}
|
||||
cardExtras={cardExtras}
|
||||
cardExtras={
|
||||
matchExtras || cardExtras ? (
|
||||
<>
|
||||
{matchExtras}
|
||||
{cardExtras}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
placement={placement}
|
||||
enterDelay={1000}
|
||||
leaveDelay={500}
|
||||
triggerClassName={triggerClassName}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
onOpen={() => {
|
||||
setIsOpened(true);
|
||||
onOpen?.();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsOpened(false);
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PerformerPopover>
|
||||
|
|
|
|||
Loading…
Reference in a new issue