mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 21:04: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 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>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -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: "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
Reference in New Issue
Block a user