performer: stashbox: show age, gender, and image (#3964)

* performer: stashbox: show age, gender, and image
* Add flag, improve styling
---------
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
This commit is contained in:
StashPRs
2023-07-31 23:59:50 -05:00
committed by GitHub
parent ab4f56213f
commit 50db9466cb
3 changed files with 172 additions and 17 deletions

View File

@@ -1,6 +1,6 @@
import React, { useEffect, useRef, useState } from "react"; import React, { useEffect, useRef, useState } from "react";
import { Button, Form } from "react-bootstrap"; import { Form, Row, Col, Badge } from "react-bootstrap";
import { useIntl } from "react-intl"; import { FormattedMessage, useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql"; import * as GQL from "src/core/generated-graphql";
import { ModalComponent } from "src/components/Shared/Modal"; import { ModalComponent } from "src/components/Shared/Modal";
@@ -8,8 +8,135 @@ import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { stashboxDisplayName } from "src/utils/stashbox"; import { stashboxDisplayName } from "src/utils/stashbox";
import { useDebouncedSetState } from "src/hooks/debounce"; import { useDebouncedSetState } from "src/hooks/debounce";
import { TruncatedText } from "src/components/Shared/TruncatedText";
import { stringToGender } from "src/utils/gender";
import TextUtils from "src/utils/text";
import GenderIcon from "src/components/Performers/GenderIcon";
import { CountryFlag } from "src/components/Shared/CountryFlag";
const CLASSNAME = "PerformerScrapeModal"; const CLASSNAME = "PerformerScrapeModal";
const CLASSNAME_LIST = `${CLASSNAME}-list`; const CLASSNAME_LIST = `${CLASSNAME}-list`;
const CLASSNAME_LIST_CONTAINER = `${CLASSNAME_LIST}-container`;
interface IPerformerSearchResultDetailsProps {
performer: GQL.ScrapedPerformerDataFragment;
}
const PerformerSearchResultDetails: React.FC<
IPerformerSearchResultDetailsProps
> = ({ performer }) => {
function renderImage() {
if (performer.images && performer.images.length > 0) {
return (
<div className="scene-image-container">
<img
src={performer.images[0]}
alt=""
className="align-self-center scene-image"
/>
</div>
);
}
}
function calculateAge() {
if (performer?.birthdate) {
// calculate the age from birthdate. In future, this should probably be
// provided by the server
return TextUtils.age(performer.birthdate, performer.death_date);
}
}
function renderTags() {
if (performer.tags) {
return (
<Row>
<Col>
{performer.tags?.map((tag) => (
<Badge
className="tag-item"
variant="secondary"
key={tag.stored_id}
>
{tag.name}
</Badge>
))}
</Col>
</Row>
);
}
}
function renderCountry() {
if (performer.country) {
return (
<span>
<CountryFlag
className="performer-result__country-flag"
country={performer.country}
/>
</span>
);
}
}
let age = calculateAge();
return (
<div className="performer-result">
<Row>
{renderImage()}
<div className="col flex-column">
<h4 className="performer-name">
<span>{performer.name}</span>
{performer.disambiguation && (
<span className="performer-disambiguation">
{` (${performer.disambiguation})`}
</span>
)}
</h4>
<h5 className="performer-details">
{performer.gender && (
<span>
<GenderIcon
className="gender-icon"
gender={stringToGender(performer.gender, true)}
/>
</span>
)}
{age && (
<span>
{`${age} `}
<FormattedMessage id="years_old" />
</span>
)}
</h5>
{renderCountry()}
</div>
</Row>
<Row>
<Col>
<TruncatedText text={performer.details ?? ""} lineCount={3} />
</Col>
</Row>
{renderTags()}
</div>
);
};
export interface IPerformerSearchResult {
performer: GQL.ScrapedPerformerDataFragment;
}
export const PerformerSearchResult: React.FC<IPerformerSearchResult> = ({
performer,
}) => {
return (
<div className="mt-3 search-item">
<PerformerSearchResultDetails performer={performer} />
</div>
);
};
export interface IStashBox extends GQL.StashBox { export interface IStashBox extends GQL.StashBox {
index: number; index: number;
@@ -48,6 +175,31 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
useEffect(() => inputRef.current?.focus(), []); useEffect(() => inputRef.current?.focus(), []);
function renderResults() {
if (!performers) {
return;
}
return (
<div className={CLASSNAME_LIST_CONTAINER}>
<div className="mt-1">
<FormattedMessage
id="dialogs.performers_found"
values={{ count: performers.length }}
/>
</div>
<ul className={CLASSNAME_LIST}>
{performers.map((p, i) => (
// eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-noninteractive-element-interactions, react/no-array-index-key
<li key={i} onClick={() => onSelectPerformer(p)}>
<PerformerSearchResult performer={p} />
</li>
))}
</ul>
</div>
);
}
return ( return (
<ModalComponent <ModalComponent
show show
@@ -75,16 +227,7 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
<LoadingIndicator inline /> <LoadingIndicator inline />
</div> </div>
) : performers.length > 0 ? ( ) : performers.length > 0 ? (
<ul className={CLASSNAME_LIST}> renderResults()
{performers.map((p) => (
<li key={p.remote_site_id}>
<Button variant="link" onClick={() => onSelectPerformer(p)}>
{p.name}
{p.disambiguation && ` (${p.disambiguation})`}
</Button>
</li>
))}
</ul>
) : ( ) : (
query !== "" && <h5 className="text-center">No results found.</h5> query !== "" && <h5 className="text-center">No results found.</h5>
)} )}

View File

@@ -158,13 +158,14 @@
.PerformerScrapeModal { .PerformerScrapeModal {
&-list { &-list {
list-style-type: none; list-style: none;
max-height: 50vh; max-height: 50vh;
overflow-x: auto; overflow-x: hidden;
padding-left: 1rem; overflow-y: auto;
padding-inline-start: 0;
.btn { li {
font-size: 1.2rem; cursor: pointer;
} }
} }
} }
@@ -212,3 +213,13 @@
/* stylelint-enable */ /* stylelint-enable */
padding-right: 0.5rem; padding-right: 0.5rem;
} }
.performer-result .performer-details > span {
&::after {
content: "";
}
&:last-child::after {
content: "";
}
}

View File

@@ -888,6 +888,7 @@
"video_previews_tooltip": "Video previews which play when hovering over a scene" "video_previews_tooltip": "Video previews which play when hovering over a scene"
}, },
"scenes_found": "{count} scenes found", "scenes_found": "{count} scenes found",
"performers_found": "{count} performers found",
"scrape_entity_query": "{entity_type} Scrape Query", "scrape_entity_query": "{entity_type} Scrape Query",
"scrape_entity_title": "{entity_type} Scrape Results", "scrape_entity_title": "{entity_type} Scrape Results",
"scrape_results_existing": "Existing", "scrape_results_existing": "Existing",