mirror of
https://github.com/stashapp/stash.git
synced 2026-05-09 05:05:29 +02:00
Merge 78cd9c36f3 into 01a7583364
This commit is contained in:
commit
7e1c33b151
7 changed files with 647 additions and 35 deletions
|
|
@ -8,15 +8,20 @@ import { useConfigurationContext } from "../../hooks/Config";
|
|||
import { Placement } from "react-bootstrap/esm/Overlay";
|
||||
|
||||
interface IPeromerPopoverCardProps {
|
||||
id: string;
|
||||
id?: string;
|
||||
cardContent?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
cardExtras?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const PerformerPopoverCard: React.FC<IPeromerPopoverCardProps> = ({
|
||||
id,
|
||||
}) => {
|
||||
const { data, loading, error } = useFindPerformer(id);
|
||||
const PerformerPopoverCardByID: React.FC<{
|
||||
id: string;
|
||||
cardExtras?: React.ReactNode;
|
||||
}> = ({ id, cardExtras }) => {
|
||||
const { data, loading: isLoading, error } = useFindPerformer(id);
|
||||
|
||||
if (loading)
|
||||
if (isLoading)
|
||||
return (
|
||||
<div className="tag-popover-card-placeholder">
|
||||
<LoadingIndicator card={true} message={""} />
|
||||
|
|
@ -29,25 +34,77 @@ export const PerformerPopoverCard: React.FC<IPeromerPopoverCardProps> = ({
|
|||
const performer = data.findPerformer;
|
||||
|
||||
return (
|
||||
<div className="tag-popover-card">
|
||||
<PerformerCard performer={performer} zoomIndex={0} />
|
||||
</div>
|
||||
<>
|
||||
<div className="tag-popover-card">
|
||||
<PerformerCard performer={performer} zoomIndex={0} />
|
||||
</div>
|
||||
{cardExtras}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export const PerformerPopoverCard: React.FC<IPeromerPopoverCardProps> = ({
|
||||
id,
|
||||
cardContent,
|
||||
loading,
|
||||
loadingText = "",
|
||||
cardExtras,
|
||||
}) => {
|
||||
if (cardContent) {
|
||||
return (
|
||||
<>
|
||||
{cardContent}
|
||||
{cardExtras}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<>
|
||||
<div className="tag-popover-card performer-preview-popover p-3">
|
||||
{loadingText}
|
||||
</div>
|
||||
{cardExtras}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
if (!id) return null;
|
||||
return <PerformerPopoverCardByID id={id} cardExtras={cardExtras} />;
|
||||
};
|
||||
|
||||
interface IPeroformerPopoverProps {
|
||||
id: string;
|
||||
id?: string;
|
||||
cardContent?: React.ReactNode;
|
||||
loading?: boolean;
|
||||
loadingText?: string;
|
||||
cardExtras?: React.ReactNode;
|
||||
hide?: boolean;
|
||||
placement?: Placement;
|
||||
enterDelay?: number;
|
||||
leaveDelay?: number;
|
||||
target?: React.RefObject<HTMLElement>;
|
||||
triggerClassName?: string;
|
||||
onOpen?: () => void;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const PerformerPopover: React.FC<IPeroformerPopoverProps> = ({
|
||||
id,
|
||||
cardContent,
|
||||
loading,
|
||||
loadingText,
|
||||
cardExtras,
|
||||
hide,
|
||||
children,
|
||||
placement = "top",
|
||||
enterDelay = 500,
|
||||
leaveDelay = 100,
|
||||
target,
|
||||
triggerClassName,
|
||||
onOpen,
|
||||
onClose,
|
||||
}) => {
|
||||
const { configuration: config } = useConfigurationContext();
|
||||
|
||||
|
|
@ -59,11 +116,22 @@ export const PerformerPopover: React.FC<IPeroformerPopoverProps> = ({
|
|||
|
||||
return (
|
||||
<HoverPopover
|
||||
className={triggerClassName}
|
||||
target={target}
|
||||
placement={placement}
|
||||
enterDelay={500}
|
||||
leaveDelay={100}
|
||||
content={<PerformerPopoverCard id={id} />}
|
||||
enterDelay={enterDelay}
|
||||
leaveDelay={leaveDelay}
|
||||
onOpen={onOpen}
|
||||
onClose={onClose}
|
||||
content={
|
||||
<PerformerPopoverCard
|
||||
id={id}
|
||||
cardContent={cardContent}
|
||||
loading={loading}
|
||||
loadingText={loadingText}
|
||||
cardExtras={cardExtras}
|
||||
/>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</HoverPopover>
|
||||
|
|
|
|||
|
|
@ -158,6 +158,16 @@
|
|||
}
|
||||
}
|
||||
|
||||
.performer-preview-popover {
|
||||
.card {
|
||||
width: 200px;
|
||||
}
|
||||
|
||||
.performer-card-image {
|
||||
max-height: 300px;
|
||||
}
|
||||
}
|
||||
|
||||
.scrape-dialog .performer-image {
|
||||
display: block;
|
||||
margin-bottom: 10px;
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@ import { faGripLines } from "@fortawesome/free-solid-svg-icons";
|
|||
import { DragSide, useDragMoveSelect } from "./dragMoveSelect";
|
||||
import { useDebounce } from "src/hooks/debounce";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import { ExternalLink } from "../ExternalLink";
|
||||
|
||||
interface ICardProps {
|
||||
className?: string;
|
||||
|
|
@ -172,6 +173,27 @@ const MoveTarget: React.FC<{ dragSide: DragSide }> = ({ dragSide }) => {
|
|||
);
|
||||
};
|
||||
|
||||
function CardNavLink(props: {
|
||||
url: string;
|
||||
linkClassName?: string;
|
||||
onClick: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const { url, linkClassName, onClick, children } = props;
|
||||
if (/^https?:\/\//i.test(url)) {
|
||||
return (
|
||||
<ExternalLink href={url} className={linkClassName} onClick={onClick}>
|
||||
{children}
|
||||
</ExternalLink>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<Link to={url} className={linkClassName} onClick={onClick}>
|
||||
{children}
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
export const GridCard: React.FC<ICardProps> = PatchComponent(
|
||||
"GridCard",
|
||||
(props: ICardProps) => {
|
||||
|
|
@ -256,24 +278,24 @@ export const GridCard: React.FC<ICardProps> = PatchComponent(
|
|||
<div
|
||||
className={cx(props.thumbnailSectionClassName, "thumbnail-section")}
|
||||
>
|
||||
<Link
|
||||
to={props.url}
|
||||
className={props.linkClassName}
|
||||
<CardNavLink
|
||||
url={props.url}
|
||||
linkClassName={props.linkClassName}
|
||||
onClick={handleImageClick}
|
||||
>
|
||||
{props.image}
|
||||
</Link>
|
||||
</CardNavLink>
|
||||
{props.overlays}
|
||||
{maybeRenderProgressBar()}
|
||||
</div>
|
||||
{maybeRenderInteractiveHeatmap()}
|
||||
<div className="card-section">
|
||||
<Link to={props.url} onClick={handleImageClick}>
|
||||
<CardNavLink url={props.url} onClick={handleImageClick}>
|
||||
<h5 className="card-section-title flex-aligned">
|
||||
{props.pretitleIcon}
|
||||
<TruncatedText text={props.title} lineCount={2} />
|
||||
</h5>
|
||||
</Link>
|
||||
</CardNavLink>
|
||||
{props.details}
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -12,6 +12,7 @@ import { getStashboxBase } from "src/utils/stashbox";
|
|||
import { ExternalLink } from "src/components/Shared/ExternalLink";
|
||||
import { Link } from "react-router-dom";
|
||||
import { LinkButton } from "../LinkButton";
|
||||
import { TaggerPerformerPopover } from "./TaggerPerformerPopover";
|
||||
|
||||
const PerformerLink: React.FC<{
|
||||
performer: GQL.ScrapedPerformer | Performer;
|
||||
|
|
@ -73,7 +74,6 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||
stashID.endpoint === endpoint &&
|
||||
stashID.stash_id === performer.remote_site_id
|
||||
);
|
||||
|
||||
const [selectedPerformer, setSelectedPerformer] = useState<Performer>();
|
||||
|
||||
const stashboxPerformerPrefix = endpoint
|
||||
|
|
@ -115,10 +115,15 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||
<div className="entity-name">
|
||||
<FormattedMessage id="countables.performers" values={{ count: 1 }} />:
|
||||
<b className="ml-2">
|
||||
<PerformerLink
|
||||
performer={performer}
|
||||
url={`${stashboxPerformerPrefix}${performer.remote_site_id}`}
|
||||
/>
|
||||
<TaggerPerformerPopover
|
||||
scrapedPerformer={performer}
|
||||
endpoint={endpoint}
|
||||
>
|
||||
<PerformerLink
|
||||
performer={performer}
|
||||
url={`${stashboxPerformerPrefix}${performer.remote_site_id}`}
|
||||
/>
|
||||
</TaggerPerformerPopover>
|
||||
</b>
|
||||
</div>
|
||||
<span className="ml-auto">
|
||||
|
|
@ -159,10 +164,15 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||
<div className="entity-name">
|
||||
<FormattedMessage id="countables.performers" values={{ count: 1 }} />:
|
||||
<b className="ml-2">
|
||||
<PerformerLink
|
||||
performer={performer}
|
||||
url={safeBuildPerformerScraperLink(performer.remote_site_id)}
|
||||
/>
|
||||
<TaggerPerformerPopover
|
||||
scrapedPerformer={performer}
|
||||
endpoint={endpoint}
|
||||
>
|
||||
<PerformerLink
|
||||
performer={performer}
|
||||
url={safeBuildPerformerScraperLink(performer.remote_site_id)}
|
||||
/>
|
||||
</TaggerPerformerPopover>
|
||||
</b>
|
||||
</div>
|
||||
<ButtonGroup>
|
||||
|
|
@ -175,13 +185,20 @@ const PerformerResult: React.FC<IPerformerResultProps> = ({
|
|||
>
|
||||
<FormattedMessage id="actions.skip" />
|
||||
</Button>
|
||||
<PerformerSelect
|
||||
values={selectedPerformer ? [selectedPerformer] : []}
|
||||
onSelect={handleSelect}
|
||||
active={selectedSource === "existing"}
|
||||
isClearable={false}
|
||||
ageFromDate={ageFromDate}
|
||||
/>
|
||||
<TaggerPerformerPopover
|
||||
performerID={selectedPerformer?.id}
|
||||
scrapedPerformer={performer}
|
||||
endpoint={endpoint}
|
||||
includeMatchExtras
|
||||
>
|
||||
<PerformerSelect
|
||||
values={selectedPerformer ? [selectedPerformer] : []}
|
||||
onSelect={handleSelect}
|
||||
active={selectedSource === "existing"}
|
||||
isClearable={false}
|
||||
ageFromDate={ageFromDate}
|
||||
/>
|
||||
</TaggerPerformerPopover>
|
||||
{endpoint && onLink && (
|
||||
<LinkButton disabled={selectedID === undefined} onLink={onLink} />
|
||||
)}
|
||||
|
|
|
|||
145
ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerCard.tsx
Normal file
145
ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerCard.tsx
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { stringToGender } from "src/utils/gender";
|
||||
import { getStashboxBase } from "src/utils/stashbox";
|
||||
import { GridCard } from "src/components/Shared/GridCard/GridCard";
|
||||
import { CountryFlag } from "src/components/Shared/CountryFlag";
|
||||
import { PatchComponent } from "src/patch";
|
||||
import GenderIcon from "src/components/Performers/GenderIcon";
|
||||
|
||||
export interface IScrapedPerformerCardProps {
|
||||
scrapedPerformer: GQL.ScrapedPerformer;
|
||||
endpoint: string;
|
||||
cardWidth?: number;
|
||||
ageFromDate?: string | null;
|
||||
selecting?: boolean;
|
||||
selected?: boolean;
|
||||
zoomIndex?: number;
|
||||
onSelectedChanged?: (selected: boolean, shiftKey: boolean) => void;
|
||||
}
|
||||
|
||||
const ScrapedPerformerCardOverlays: React.FC<IScrapedPerformerCardProps> =
|
||||
PatchComponent("ScrapedPerformerCard.Overlays", ({ scrapedPerformer }) => {
|
||||
function maybeRenderFlag() {
|
||||
if (!scrapedPerformer.country) {
|
||||
return;
|
||||
}
|
||||
return (
|
||||
<CountryFlag
|
||||
className="performer-card__country-flag"
|
||||
country={scrapedPerformer.country}
|
||||
includeOverlay
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return <>{maybeRenderFlag()}</>;
|
||||
});
|
||||
|
||||
const ScrapedPerformerCardDetails: React.FC<IScrapedPerformerCardProps> =
|
||||
PatchComponent("ScrapedPerformerCard.Details", (props) => {
|
||||
const { scrapedPerformer, ageFromDate } = props;
|
||||
const intl = useIntl();
|
||||
const age = TextUtils.age(
|
||||
scrapedPerformer.birthdate,
|
||||
ageFromDate ?? scrapedPerformer.death_date
|
||||
);
|
||||
const ageL10nId = ageFromDate
|
||||
? "media_info.performer_card.age_context"
|
||||
: "media_info.performer_card.age";
|
||||
const ageL10String = intl.formatMessage({
|
||||
id: "years_old",
|
||||
});
|
||||
const ageString = intl.formatMessage(
|
||||
{ id: ageL10nId },
|
||||
{ age, years_old: ageL10String }
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
{age !== 0 ? (
|
||||
<div className="performer-card__age">{ageString}</div>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
</>
|
||||
);
|
||||
});
|
||||
|
||||
const ScrapedPerformerCardImage: React.FC<IScrapedPerformerCardProps> =
|
||||
PatchComponent("ScrapedPerformerCard.Image", ({ scrapedPerformer }) => {
|
||||
const intl = useIntl();
|
||||
const unknownName = intl.formatMessage({
|
||||
id: "component_tagger.results.unnamed",
|
||||
});
|
||||
const alt = scrapedPerformer.name ?? unknownName;
|
||||
return (
|
||||
<img
|
||||
loading="lazy"
|
||||
className="performer-card-image"
|
||||
alt={alt}
|
||||
src={scrapedPerformer.images?.[0] ?? ""}
|
||||
/>
|
||||
);
|
||||
});
|
||||
|
||||
const ScrapedPerformerCardTitle: React.FC<IScrapedPerformerCardProps> =
|
||||
PatchComponent("ScrapedPerformerCard.Title", ({ scrapedPerformer }) => {
|
||||
const intl = useIntl();
|
||||
const unknownPerformerName = intl.formatMessage({
|
||||
id: "component_tagger.results.unnamed",
|
||||
});
|
||||
const name = scrapedPerformer.name ?? unknownPerformerName;
|
||||
return (
|
||||
<div>
|
||||
<span className="performer-name">{name}</span>
|
||||
{scrapedPerformer.disambiguation && (
|
||||
<span className="performer-disambiguation">
|
||||
{` (${scrapedPerformer.disambiguation})`}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export const ScrapedPerformerCard: React.FC<IScrapedPerformerCardProps> =
|
||||
PatchComponent("ScrapedPerformerCard", (props) => {
|
||||
const {
|
||||
scrapedPerformer,
|
||||
endpoint,
|
||||
cardWidth,
|
||||
selecting,
|
||||
selected,
|
||||
onSelectedChanged,
|
||||
zoomIndex,
|
||||
} = props;
|
||||
|
||||
const base = getStashboxBase(endpoint);
|
||||
const stashboxUrl =
|
||||
base && scrapedPerformer.remote_site_id
|
||||
? `${base}performers/${scrapedPerformer.remote_site_id}`
|
||||
: "#";
|
||||
|
||||
return (
|
||||
<GridCard
|
||||
className={`performer-card zoom-${zoomIndex}`}
|
||||
url={stashboxUrl}
|
||||
width={cardWidth}
|
||||
pretitleIcon={
|
||||
<GenderIcon
|
||||
className="gender-icon"
|
||||
gender={stringToGender(scrapedPerformer.gender, true)}
|
||||
/>
|
||||
}
|
||||
title={<ScrapedPerformerCardTitle {...props} />}
|
||||
image={<ScrapedPerformerCardImage {...props} />}
|
||||
overlays={<ScrapedPerformerCardOverlays {...props} />}
|
||||
details={<ScrapedPerformerCardDetails {...props} />}
|
||||
selected={selected}
|
||||
selecting={selecting}
|
||||
onSelectedChanged={onSelectedChanged}
|
||||
/>
|
||||
);
|
||||
});
|
||||
304
ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx
Normal file
304
ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx
Normal file
|
|
@ -0,0 +1,304 @@
|
|||
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;
|
||||
onClose?: () => void;
|
||||
}
|
||||
|
||||
export const TaggerPerformerPopover: React.FC<
|
||||
React.PropsWithChildren<ITaggerPerformerPopoverProps>
|
||||
> = ({
|
||||
performer,
|
||||
performerID,
|
||||
scrapedPerformer,
|
||||
endpoint,
|
||||
cardExtras,
|
||||
includeMatchExtras = false,
|
||||
placement = "bottom",
|
||||
triggerClassName = "d-inline-block",
|
||||
onOpen,
|
||||
onClose,
|
||||
children,
|
||||
}) => {
|
||||
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={localPerformer} zoomIndex={0} />
|
||||
</div>
|
||||
) : scrapedPerformer ? (
|
||||
<div className="tag-popover-card tagger-performer-popover-card">
|
||||
<ScrapedPerformerCard
|
||||
scrapedPerformer={scrapedPerformer}
|
||||
endpoint={endpoint ?? ""}
|
||||
zoomIndex={0}
|
||||
/>
|
||||
</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={
|
||||
matchExtras || cardExtras ? (
|
||||
<>
|
||||
{matchExtras}
|
||||
{cardExtras}
|
||||
</>
|
||||
) : null
|
||||
}
|
||||
placement={placement}
|
||||
enterDelay={1000}
|
||||
leaveDelay={500}
|
||||
triggerClassName={triggerClassName}
|
||||
onOpen={() => {
|
||||
setIsOpened(true);
|
||||
onOpen?.();
|
||||
}}
|
||||
onClose={() => {
|
||||
setIsOpened(false);
|
||||
onClose?.();
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</PerformerPopover>
|
||||
);
|
||||
};
|
||||
|
|
@ -95,6 +95,52 @@
|
|||
}
|
||||
}
|
||||
|
||||
.tagger-matched-performer-popover-extra {
|
||||
border-top: 1px solid $secondary;
|
||||
margin-top: 0.4rem;
|
||||
padding-bottom: 0.4rem;
|
||||
padding-left: 0.5rem;
|
||||
padding-right: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-height: 1049px) {
|
||||
.tagger-performer-popover-card {
|
||||
.card {
|
||||
max-width: 160px;
|
||||
width: 160px;
|
||||
}
|
||||
|
||||
.card-section-title {
|
||||
font-size: 1rem;
|
||||
}
|
||||
|
||||
// Target BEM class from shared PerformerCard markup; cannot
|
||||
// be renamed here without changing reusable performer card components.
|
||||
// stylelint-disable-next-line selector-class-pattern
|
||||
.performer-card__age {
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.selected-result {
|
||||
background-color: hsl(204, 20%, 30%);
|
||||
border-radius: 3px;
|
||||
|
|
|
|||
Loading…
Reference in a new issue