diff --git a/ui/v2.5/src/components/Performers/PerformerPopover.tsx b/ui/v2.5/src/components/Performers/PerformerPopover.tsx index 287900b09..b28f73e9f 100644 --- a/ui/v2.5/src/components/Performers/PerformerPopover.tsx +++ b/ui/v2.5/src/components/Performers/PerformerPopover.tsx @@ -13,6 +13,7 @@ import { interface IPeromerPopoverCardProps { id?: string; + cardContent?: React.ReactNode; previewData?: IPerformerPreviewData; loading?: boolean; loadingText?: string; @@ -49,11 +50,21 @@ const PerformerPopoverCardByID: React.FC<{ export const PerformerPopoverCard: React.FC = ({ id, + cardContent, previewData, loading, loadingText = "", cardExtras, }) => { + if (cardContent) { + return ( + <> + {cardContent} + {cardExtras} + + ); + } + if (previewData || loading) { return ( <> @@ -75,12 +86,15 @@ export const PerformerPopoverCard: React.FC = ({ interface IPeroformerPopoverProps { id?: string; + cardContent?: React.ReactNode; previewData?: IPerformerPreviewData; loading?: boolean; loadingText?: string; cardExtras?: React.ReactNode; hide?: boolean; placement?: Placement; + enterDelay?: number; + leaveDelay?: number; target?: React.RefObject; triggerClassName?: string; onOpen?: () => void; @@ -89,6 +103,7 @@ interface IPeroformerPopoverProps { export const PerformerPopover: React.FC = ({ id, + cardContent, previewData, loading, loadingText, @@ -96,6 +111,8 @@ export const PerformerPopover: React.FC = ({ hide, children, placement = "top", + enterDelay = 500, + leaveDelay = 100, target, triggerClassName, onOpen, @@ -114,13 +131,14 @@ export const PerformerPopover: React.FC = ({ className={triggerClassName} target={target} placement={placement} - enterDelay={500} - leaveDelay={100} + enterDelay={enterDelay} + leaveDelay={leaveDelay} onOpen={onOpen} onClose={onClose} content={ = ({ dragSide }) => { ); }; +function CardNavLink(props: { + url: string; + linkClassName?: string; + onClick: (event: React.MouseEvent) => void; + children: React.ReactNode; +}) { + const { url, linkClassName, onClick, children } = props; + if (/^https?:\/\//i.test(url)) { + return ( + + {children} + + ); + } + return ( + + {children} + + ); +} + export const GridCard: React.FC = PatchComponent( "GridCard", (props: ICardProps) => { @@ -256,24 +278,24 @@ export const GridCard: React.FC = PatchComponent(
- {props.image} - + {props.overlays} {maybeRenderProgressBar()}
{maybeRenderInteractiveHeatmap()}
- +
{props.pretitleIcon}
- +
{props.details}
diff --git a/ui/v2.5/src/components/Tagger/scenes/MatchedPerformerPreview.tsx b/ui/v2.5/src/components/Tagger/scenes/MatchedPerformerPreview.tsx index c0dbec4d1..afe74ca90 100644 --- a/ui/v2.5/src/components/Tagger/scenes/MatchedPerformerPreview.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/MatchedPerformerPreview.tsx @@ -2,8 +2,8 @@ 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 { PerformerPopover } from "src/components/Performers/PerformerPopover"; import { useConfigurationContext } from "src/hooks/Config"; +import { TaggerPerformerPopover } from "./TaggerPerformerPopover"; interface IPerformerDeltaRow { label: string; @@ -215,8 +215,9 @@ export const MatchedPerformerPreview = ({ } return ( - 0 ? (
@@ -250,6 +251,6 @@ export const MatchedPerformerPreview = ({ 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 859ae2b83..f71cdae3f 100755 --- a/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx +++ b/ui/v2.5/src/components/Tagger/scenes/PerformerResult.tsx @@ -13,7 +13,7 @@ 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"; +import { TaggerPerformerPopover } from "./TaggerPerformerPopover"; const PerformerLink: React.FC<{ performer: GQL.ScrapedPerformer | Performer; @@ -116,12 +116,15 @@ const PerformerResult: React.FC = ({
: - + - +
@@ -162,12 +165,15 @@ const PerformerResult: React.FC = ({
: - + - +
diff --git a/ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerCard.tsx b/ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerCard.tsx new file mode 100644 index 000000000..f9f057421 --- /dev/null +++ b/ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerCard.tsx @@ -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 = + PatchComponent("ScrapedPerformerCard.Overlays", ({ scrapedPerformer }) => { + function maybeRenderFlag() { + if (!scrapedPerformer.country) { + return; + } + return ( + + ); + } + + return <>{maybeRenderFlag()}; + }); + +const ScrapedPerformerCardDetails: React.FC = + 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 ? ( +
{ageString}
+ ) : ( + "" + )} + + ); + }); + +const ScrapedPerformerCardImage: React.FC = + PatchComponent("ScrapedPerformerCard.Image", ({ scrapedPerformer }) => { + const intl = useIntl(); + const unknownName = intl.formatMessage({ + id: "component_tagger.results.unnamed", + }); + const alt = scrapedPerformer.name ?? unknownName; + return ( + {alt} + ); + }); + +const ScrapedPerformerCardTitle: React.FC = + PatchComponent("ScrapedPerformerCard.Title", ({ scrapedPerformer }) => { + const intl = useIntl(); + const unknownPerformerName = intl.formatMessage({ + id: "component_tagger.results.unnamed", + }); + const name = scrapedPerformer.name ?? unknownPerformerName; + return ( +
+ {name} + {scrapedPerformer.disambiguation && ( + + {` (${scrapedPerformer.disambiguation})`} + + )} +
+ ); + }); + +export const ScrapedPerformerCard: React.FC = + 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 ( + + } + title={} + image={} + overlays={} + details={} + selected={selected} + selecting={selecting} + onSelectedChanged={onSelectedChanged} + /> + ); + }); diff --git a/ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerPreview.tsx b/ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerPreview.tsx deleted file mode 100644 index fb570e33c..000000000 --- a/ui/v2.5/src/components/Tagger/scenes/ScrapedPerformerPreview.tsx +++ /dev/null @@ -1,78 +0,0 @@ -import { ReactNode } from "react"; -import { Placement } from "react-bootstrap/esm/Overlay"; -import { useIntl } from "react-intl"; -import * as GQL from "src/core/generated-graphql"; -import { IPerformerPreviewData } from "src/components/Performers/PerformerPreviewCard"; -import { PerformerPopover } from "src/components/Performers/PerformerPopover"; -import TextUtils from "src/utils/text"; -import { stringToGender } from "src/utils/gender"; - -interface IScrapedPerformerPreviewProps { - performer: GQL.ScrapedPerformer; - placement?: Placement; - children?: ReactNode; -} - -export const localPerformerToPreviewData = ( - performer: GQL.PerformerDataFragment, - ageString: string | null -): IPerformerPreviewData => ({ - name: performer.name, - image: performer.image_path, - country: performer.country, - gender: performer.gender, - disambiguation: performer.disambiguation, - ageString, -}); - -export const scrapedPerformerToPreviewData = ( - performer: GQL.ScrapedPerformer, - name: string, - ageString: string | null -): IPerformerPreviewData => ({ - name, - image: performer.images?.[0], - country: performer.country, - gender: stringToGender(performer.gender, true), - disambiguation: performer.disambiguation, - ageString, -}); - -export const ScrapedPerformerPreview = ({ - performer, - placement = "bottom", - children, -}: IScrapedPerformerPreviewProps) => { - const intl = useIntl(); - const unknownPerformerName = intl.formatMessage({ - id: "component_tagger.results.unnamed", - defaultMessage: "Unnamed", - }); - const name = performer.name ?? unknownPerformerName; - const age = TextUtils.age(performer.birthdate, performer.death_date); - const ageString = intl.formatMessage( - { id: "media_info.performer_card.age" }, - { - age, - years_old: intl.formatMessage({ - id: "years_old", - defaultMessage: "years old", - }), - } - ); - const previewData = scrapedPerformerToPreviewData( - performer, - name, - age !== 0 ? ageString : null - ); - - return ( - - {children} - - ); -}; diff --git a/ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx b/ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx new file mode 100644 index 000000000..7f278db62 --- /dev/null +++ b/ui/v2.5/src/components/Tagger/scenes/TaggerPerformerPopover.tsx @@ -0,0 +1,63 @@ +import React from "react"; +import * as GQL from "src/core/generated-graphql"; +import { Placement } from "react-bootstrap/esm/Overlay"; +import { PerformerPopover } from "src/components/Performers/PerformerPopover"; +import { PerformerCard } from "src/components/Performers/PerformerCard"; +import { ScrapedPerformerCard } from "./ScrapedPerformerCard"; + +interface ITaggerPerformerPopoverProps { + performer?: GQL.PerformerDataFragment; + performerID?: string; + scrapedPerformer?: GQL.ScrapedPerformer; + endpoint?: string; + cardExtras?: React.ReactNode; + placement?: Placement; + triggerClassName?: string; + onOpen?: () => void; + onClose?: () => void; +} + +export const TaggerPerformerPopover: React.FC< + React.PropsWithChildren +> = ({ + performer, + performerID, + scrapedPerformer, + endpoint, + cardExtras, + placement = "bottom", + triggerClassName = "d-inline-block", + onOpen, + onClose, + children, +}) => { + const cardContent = performer ? ( +
+ +
+ ) : scrapedPerformer ? ( +
+ +
+ ) : undefined; + + return ( + + {children} + + ); +}; diff --git a/ui/v2.5/src/components/Tags/styles.scss b/ui/v2.5/src/components/Tags/styles.scss index 2e6b73d73..9038c52a0 100644 --- a/ui/v2.5/src/components/Tags/styles.scss +++ b/ui/v2.5/src/components/Tags/styles.scss @@ -125,6 +125,23 @@ } } +@media (max-height: 1049px) { + .tag-popover-card { + .card { + max-width: 160px; + width: 160px; + } + + .card-section-title { + font-size: 1rem; + } + + .performer-card__age { + font-size: 0.9rem; + } + } +} + .tag-item { .icon-wrapper { color: #202b33;