mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
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:
@@ -1,6 +1,6 @@
|
||||
import React, { useEffect, useRef, useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Form, Row, Col, Badge } from "react-bootstrap";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
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 { 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_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 {
|
||||
index: number;
|
||||
@@ -48,6 +175,31 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
|
||||
|
||||
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 (
|
||||
<ModalComponent
|
||||
show
|
||||
@@ -75,16 +227,7 @@ const PerformerStashBoxModal: React.FC<IProps> = ({
|
||||
<LoadingIndicator inline />
|
||||
</div>
|
||||
) : performers.length > 0 ? (
|
||||
<ul className={CLASSNAME_LIST}>
|
||||
{performers.map((p) => (
|
||||
<li key={p.remote_site_id}>
|
||||
<Button variant="link" onClick={() => onSelectPerformer(p)}>
|
||||
{p.name}
|
||||
{p.disambiguation && ` (${p.disambiguation})`}
|
||||
</Button>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
renderResults()
|
||||
) : (
|
||||
query !== "" && <h5 className="text-center">No results found.</h5>
|
||||
)}
|
||||
|
||||
@@ -158,13 +158,14 @@
|
||||
|
||||
.PerformerScrapeModal {
|
||||
&-list {
|
||||
list-style-type: none;
|
||||
list-style: none;
|
||||
max-height: 50vh;
|
||||
overflow-x: auto;
|
||||
padding-left: 1rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
padding-inline-start: 0;
|
||||
|
||||
.btn {
|
||||
font-size: 1.2rem;
|
||||
li {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,3 +213,13 @@
|
||||
/* stylelint-enable */
|
||||
padding-right: 0.5rem;
|
||||
}
|
||||
|
||||
.performer-result .performer-details > span {
|
||||
&::after {
|
||||
content: " • ";
|
||||
}
|
||||
|
||||
&:last-child::after {
|
||||
content: "";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -888,6 +888,7 @@
|
||||
"video_previews_tooltip": "Video previews which play when hovering over a scene"
|
||||
},
|
||||
"scenes_found": "{count} scenes found",
|
||||
"performers_found": "{count} performers found",
|
||||
"scrape_entity_query": "{entity_type} Scrape Query",
|
||||
"scrape_entity_title": "{entity_type} Scrape Results",
|
||||
"scrape_results_existing": "Existing",
|
||||
|
||||
Reference in New Issue
Block a user