mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Add scenes tab to performer page (#280)
This commit is contained in:
@@ -1,18 +1,20 @@
|
|||||||
/* eslint-disable react/no-this-in-sfc */
|
/* eslint-disable react/no-this-in-sfc */
|
||||||
|
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { Button, Form, Spinner, Table } from "react-bootstrap";
|
import { Button, Spinner, Tabs, Tab } from "react-bootstrap";
|
||||||
import { useParams, useHistory } from "react-router-dom";
|
import { useParams, useHistory } from "react-router-dom";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { StashService } from "src/core/StashService";
|
import { StashService } from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
DetailsEditNavbar,
|
|
||||||
Icon,
|
Icon,
|
||||||
Modal,
|
|
||||||
ScrapePerformerSuggest
|
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { ImageUtils, TableUtils } from "src/utils";
|
import { useToast } from 'src/hooks';
|
||||||
import { useToast } from "src/hooks";
|
import { TextUtils } from "src/utils";
|
||||||
|
import Lightbox from 'react-images';
|
||||||
|
import { IconName } from "@fortawesome/fontawesome-svg-core";
|
||||||
|
import { PerformerDetailsPanel } from './PerformerDetailsPanel';
|
||||||
|
import { PerformerOperationsPanel } from './PerformerOperationsPanel';
|
||||||
|
import { PerformerScenesPanel } from './PerformerScenesPanel';
|
||||||
|
|
||||||
export const Performer: React.FC = () => {
|
export const Performer: React.FC = () => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
@@ -20,73 +22,19 @@ export const Performer: React.FC = () => {
|
|||||||
const { id = "new" } = useParams();
|
const { id = "new" } = useParams();
|
||||||
const isNew = id === "new";
|
const isNew = id === "new";
|
||||||
|
|
||||||
// Editing state
|
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
|
||||||
const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<GQL.Scraper>();
|
|
||||||
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<GQL.ScrapedPerformerDataFragment>();
|
|
||||||
|
|
||||||
// Editing performer state
|
|
||||||
const [image, setImage] = useState<string>();
|
|
||||||
const [name, setName] = useState<string>();
|
|
||||||
const [aliases, setAliases] = useState<string>();
|
|
||||||
const [favorite, setFavorite] = useState<boolean>();
|
|
||||||
const [birthdate, setBirthdate] = useState<string>();
|
|
||||||
const [ethnicity, setEthnicity] = useState<string>();
|
|
||||||
const [country, setCountry] = useState<string>();
|
|
||||||
const [eyeColor, setEyeColor] = useState<string>();
|
|
||||||
const [height, setHeight] = useState<string>();
|
|
||||||
const [measurements, setMeasurements] = useState<string>();
|
|
||||||
const [fakeTits, setFakeTits] = useState<string>();
|
|
||||||
const [careerLength, setCareerLength] = useState<string>();
|
|
||||||
const [tattoos, setTattoos] = useState<string>();
|
|
||||||
const [piercings, setPiercings] = useState<string>();
|
|
||||||
const [url, setUrl] = useState<string>();
|
|
||||||
const [twitter, setTwitter] = useState<string>();
|
|
||||||
const [instagram, setInstagram] = useState<string>();
|
|
||||||
|
|
||||||
// Performer state
|
// Performer state
|
||||||
const [performer, setPerformer] = useState<Partial<GQL.PerformerDataFragment>>({});
|
const [performer, setPerformer] = useState<Partial<GQL.PerformerDataFragment>>({});
|
||||||
const [imagePreview, setImagePreview] = useState<string>();
|
const [imagePreview, setImagePreview] = useState<string>();
|
||||||
|
const [lightboxIsOpen, setLightboxIsOpen] = useState(false);
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const Scrapers = StashService.useListPerformerScrapers();
|
|
||||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
|
||||||
|
|
||||||
const { data, error } = StashService.useFindPerformer(id);
|
const { data, error } = StashService.useFindPerformer(id);
|
||||||
const [updatePerformer] = StashService.usePerformerUpdate(
|
const [updatePerformer] = StashService.usePerformerUpdate();
|
||||||
getPerformerInput() as GQL.PerformerUpdateInput
|
const [createPerformer] = StashService.usePerformerCreate();
|
||||||
);
|
const [deletePerformer] = StashService.usePerformerDestroy();
|
||||||
const [createPerformer] = StashService.usePerformerCreate(
|
|
||||||
getPerformerInput() as GQL.PerformerCreateInput
|
|
||||||
);
|
|
||||||
const [deletePerformer] = StashService.usePerformerDestroy(
|
|
||||||
getPerformerInput() as GQL.PerformerDestroyInput
|
|
||||||
);
|
|
||||||
|
|
||||||
function updatePerformerEditState(
|
|
||||||
state: Partial<GQL.PerformerDataFragment | GQL.ScrapedPerformer>
|
|
||||||
) {
|
|
||||||
if ((state as GQL.PerformerDataFragment).favorite !== undefined) {
|
|
||||||
setFavorite((state as GQL.PerformerDataFragment).favorite);
|
|
||||||
}
|
|
||||||
setName(state.name ?? undefined);
|
|
||||||
setAliases(state.aliases ?? undefined);
|
|
||||||
setBirthdate(state.birthdate ?? undefined);
|
|
||||||
setEthnicity(state.ethnicity ?? undefined);
|
|
||||||
setCountry(state.country ?? undefined);
|
|
||||||
setEyeColor(state.eye_color ?? undefined);
|
|
||||||
setHeight(state.height ?? undefined);
|
|
||||||
setMeasurements(state.measurements ?? undefined);
|
|
||||||
setFakeTits(state.fake_tits ?? undefined);
|
|
||||||
setCareerLength(state.career_length ?? undefined);
|
|
||||||
setTattoos(state.tattoos ?? undefined);
|
|
||||||
setPiercings(state.piercings ?? undefined);
|
|
||||||
setUrl(state.url ?? undefined);
|
|
||||||
setTwitter(state.twitter ?? undefined);
|
|
||||||
setInstagram(state.instagram ?? undefined);
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
@@ -95,69 +43,26 @@ export const Performer: React.FC = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setImagePreview(performer.image_path ?? undefined);
|
setImagePreview(performer.image_path ?? undefined);
|
||||||
setImage(undefined);
|
|
||||||
updatePerformerEditState(performer);
|
|
||||||
if (!isNew)
|
|
||||||
setIsEditing(false);
|
|
||||||
}, [performer]);
|
}, [performer]);
|
||||||
|
|
||||||
function onImageLoad(this: FileReader) {
|
function onImageChange(image: string) {
|
||||||
setImagePreview(this.result as string);
|
setImagePreview(image);
|
||||||
setImage(this.result as string);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ImageUtils.usePasteImage(onImageLoad);
|
if ((!isNew && (!data || !data.findPerformer)) || isLoading)
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
const newQueryableScrapers = (Scrapers?.data?.listPerformerScrapers ?? []).filter(s => (
|
|
||||||
s.performer?.supported_scrapes.includes(GQL.ScrapeType.Name)
|
|
||||||
));
|
|
||||||
|
|
||||||
setQueryableScrapers(newQueryableScrapers);
|
|
||||||
}, [Scrapers]);
|
|
||||||
|
|
||||||
if ((!isNew && !isEditing && !data?.findPerformer) || isLoading)
|
|
||||||
return <Spinner animation="border" variant="light" />;
|
return <Spinner animation="border" variant="light" />;
|
||||||
|
|
||||||
if (error) return <div>{error.message}</div>;
|
if (error) return <div>{error.message}</div>;
|
||||||
|
|
||||||
function getPerformerInput() {
|
async function onSave(performerInput: Partial<GQL.PerformerCreateInput> | Partial<GQL.PerformerUpdateInput>) {
|
||||||
const performerInput: Partial<
|
|
||||||
GQL.PerformerCreateInput | GQL.PerformerUpdateInput
|
|
||||||
> = {
|
|
||||||
name,
|
|
||||||
aliases,
|
|
||||||
favorite,
|
|
||||||
birthdate,
|
|
||||||
ethnicity,
|
|
||||||
country,
|
|
||||||
eye_color: eyeColor,
|
|
||||||
height,
|
|
||||||
measurements,
|
|
||||||
fake_tits: fakeTits,
|
|
||||||
career_length: careerLength,
|
|
||||||
tattoos,
|
|
||||||
piercings,
|
|
||||||
url,
|
|
||||||
twitter,
|
|
||||||
instagram,
|
|
||||||
image
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!isNew) {
|
|
||||||
(performerInput as GQL.PerformerUpdateInput).id = id;
|
|
||||||
}
|
|
||||||
return performerInput;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave() {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
const result = await updatePerformer();
|
const result = await updatePerformer({variables: performerInput as GQL.PerformerUpdateInput});
|
||||||
if(result.data?.performerUpdate)
|
if(result.data?.performerUpdate)
|
||||||
setPerformer(result.data?.performerUpdate);
|
setPerformer(result.data?.performerUpdate);
|
||||||
} else {
|
} else {
|
||||||
const result = await createPerformer();
|
const result = await createPerformer({variables: performerInput as GQL.PerformerCreateInput});
|
||||||
if(result.data?.performerCreate) {
|
if(result.data?.performerCreate) {
|
||||||
setPerformer(result.data.performerCreate);
|
setPerformer(result.data.performerCreate);
|
||||||
history.push(`/performers/${result.data.performerCreate.id}`);
|
history.push(`/performers/${result.data.performerCreate.id}`);
|
||||||
@@ -172,7 +77,7 @@ export const Performer: React.FC = () => {
|
|||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
await deletePerformer();
|
await deletePerformer({variables: { id }});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -182,273 +87,159 @@ export const Performer: React.FC = () => {
|
|||||||
history.push("/performers");
|
history.push("/performers");
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAutoTag() {
|
function renderTabs() {
|
||||||
if (!performer.id) {
|
function renderEditPanel() {
|
||||||
return;
|
|
||||||
}
|
|
||||||
try {
|
|
||||||
await StashService.queryMetadataAutoTag({ performers: [performer.id] });
|
|
||||||
Toast.success({ content: "Started auto tagging" });
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onDisplayFreeOnesDialog(
|
|
||||||
scraper: GQL.Scraper
|
|
||||||
) {
|
|
||||||
setIsDisplayingScraperDialog(scraper);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getQueryScraperPerformerInput() {
|
|
||||||
if (!scrapePerformerDetails) return {};
|
|
||||||
|
|
||||||
const { __typename, ...ret } = scrapePerformerDetails;
|
|
||||||
debugger;
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onScrapePerformer() {
|
|
||||||
setIsDisplayingScraperDialog(undefined);
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog) return;
|
|
||||||
const result = await StashService.queryScrapePerformer(
|
|
||||||
isDisplayingScraperDialog.id,
|
|
||||||
getQueryScraperPerformerInput()
|
|
||||||
);
|
|
||||||
if (!result?.data?.scrapePerformer) return;
|
|
||||||
updatePerformerEditState(result.data.scrapePerformer);
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onScrapePerformerURL() {
|
|
||||||
if (!url) return;
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await StashService.queryScrapePerformerURL(url);
|
|
||||||
if (!result.data || !result.data.scrapePerformerURL) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
updatePerformerEditState(result.data.scrapePerformerURL);
|
|
||||||
} catch (e) {
|
|
||||||
Toast.error(e);
|
|
||||||
} finally {
|
|
||||||
setIsLoading(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderEthnicity() {
|
|
||||||
return TableUtils.renderHtmlSelect({
|
|
||||||
title: "Ethnicity",
|
|
||||||
value: ethnicity,
|
|
||||||
isEditing,
|
|
||||||
onChange: (value: string) => setEthnicity(value),
|
|
||||||
selectOptions: ["white", "black", "asian", "hispanic"]
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderScraperDialog() {
|
|
||||||
return (
|
return (
|
||||||
<Modal
|
<PerformerDetailsPanel
|
||||||
show={!!isDisplayingScraperDialog}
|
performer={performer}
|
||||||
onHide={() => setIsDisplayingScraperDialog(undefined)}
|
isEditing
|
||||||
header="Scrape"
|
isNew={isNew}
|
||||||
accept={{ onClick: onScrapePerformer, text: "Scrape" }}
|
onDelete={onDelete}
|
||||||
>
|
onSave={onSave}
|
||||||
<div className="dialog-content">
|
onImageChange={onImageChange}
|
||||||
<ScrapePerformerSuggest
|
|
||||||
placeholder="Performer name"
|
|
||||||
scraperId={
|
|
||||||
isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""
|
|
||||||
}
|
|
||||||
onSelectPerformer={query => setScrapePerformerDetails(query)}
|
|
||||||
/>
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// render tabs if not new
|
||||||
|
if (!isNew) {
|
||||||
|
return (
|
||||||
|
<Tabs defaultActiveKey="details" id="performer-details">
|
||||||
|
<Tab eventKey="details" title="Details">
|
||||||
|
<PerformerDetailsPanel performer={performer} isEditing={false} />
|
||||||
|
</Tab>
|
||||||
|
<Tab eventKey="scenes" title="Scenes">
|
||||||
|
<PerformerScenesPanel performer={performer} />
|
||||||
|
</Tab>
|
||||||
|
<Tab eventKey="edit" title="Edit">
|
||||||
|
{ renderEditPanel() }
|
||||||
|
</Tab>
|
||||||
|
<Tab eventKey="operations" title="Operations">
|
||||||
|
<PerformerOperationsPanel performer={performer} />
|
||||||
|
</Tab>
|
||||||
|
</Tabs>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return renderEditPanel();
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderAge() {
|
||||||
|
if (performer && performer.birthdate) {
|
||||||
|
// calculate the age from birthdate. In future, this should probably be
|
||||||
|
// provided by the server
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div>
|
||||||
|
<span className="age">{TextUtils.age(performer.birthdate)}</span>
|
||||||
|
<span className="age-tail"> years old</span>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function urlScrapable(scrapedUrl: string) {
|
function maybeRenderAliases() {
|
||||||
|
if (performer && performer.aliases) {
|
||||||
return (
|
return (
|
||||||
!!scrapedUrl &&
|
<>
|
||||||
(Scrapers?.data?.listPerformerScrapers ?? []).some(s =>
|
<div>
|
||||||
(s?.performer?.urls ?? []).some(u => scrapedUrl.includes(u))
|
<span className="alias-head">Also known as </span>
|
||||||
)
|
<span className="alias">{performer.aliases}</span>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderScrapeButton() {
|
|
||||||
if (!url || !isEditing || !urlScrapable(url)) {
|
|
||||||
return undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setFavorite(v : boolean) {
|
||||||
|
performer.favorite = v;
|
||||||
|
onSave(performer);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderIcons() {
|
||||||
|
function maybeRenderURL(url?: string, icon: IconName = "link") {
|
||||||
|
if (performer.url) {
|
||||||
return (
|
return (
|
||||||
<Button id="scrape-url-button" onClick={() => onScrapePerformerURL()}>
|
<Button>
|
||||||
<Icon icon="file-upload" />
|
<a href={performer.url}>
|
||||||
|
<Icon icon={icon} />
|
||||||
|
</a>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderURLField() {
|
|
||||||
return (
|
|
||||||
<tr>
|
|
||||||
<td id="url-field">
|
|
||||||
URL
|
|
||||||
{maybeRenderScrapeButton()}
|
|
||||||
</td>
|
|
||||||
<td>
|
|
||||||
<Form.Control
|
|
||||||
value={url}
|
|
||||||
readOnly={!isEditing}
|
|
||||||
plaintext={!isEditing}
|
|
||||||
placeholder="URL"
|
|
||||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
|
||||||
setUrl(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</td>
|
|
||||||
</tr>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{renderScraperDialog()}
|
<span className="name-icons">
|
||||||
<div className="row is-multiline no-spacing">
|
|
||||||
<div className="col-6 details-image-container">
|
|
||||||
<img className="performer" alt="" src={imagePreview} />
|
|
||||||
</div>
|
|
||||||
<div className="col-6 details-detail-container">
|
|
||||||
<DetailsEditNavbar
|
|
||||||
performer={performer}
|
|
||||||
isNew={isNew}
|
|
||||||
isEditing={isEditing}
|
|
||||||
onToggleEdit={() => {
|
|
||||||
setIsEditing(!isEditing);
|
|
||||||
updatePerformerEditState(performer);
|
|
||||||
}}
|
|
||||||
onSave={onSave}
|
|
||||||
onDelete={onDelete}
|
|
||||||
onImageChange={onImageChangeHandler}
|
|
||||||
scrapers={queryableScrapers}
|
|
||||||
onDisplayScraperDialog={onDisplayFreeOnesDialog}
|
|
||||||
onAutoTag={onAutoTag}
|
|
||||||
/>
|
|
||||||
<h1>
|
|
||||||
<Form.Control
|
|
||||||
readOnly={!isEditing}
|
|
||||||
plaintext={!isEditing}
|
|
||||||
defaultValue={name}
|
|
||||||
placeholder="Name"
|
|
||||||
onChange={(event: any) => setName(event.target.value)}
|
|
||||||
/>
|
|
||||||
</h1>
|
|
||||||
<h6>
|
|
||||||
<Form.Group className="aliases-field" controlId="aliases">
|
|
||||||
<Form.Label>Aliases:</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
value={aliases}
|
|
||||||
readOnly={!isEditing}
|
|
||||||
plaintext={!isEditing}
|
|
||||||
placeholder="Aliases"
|
|
||||||
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
|
||||||
setAliases(event.currentTarget.value)
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
</h6>
|
|
||||||
<div>
|
|
||||||
<span style={{ fontWeight: 300 }}>Favorite:</span>
|
|
||||||
<Button
|
<Button
|
||||||
disabled={!isEditing}
|
className={performer.favorite ? "favorite" : "not-favorite"}
|
||||||
className={favorite ? "favorite" : undefined}
|
onClick={() => setFavorite(!performer.favorite)}
|
||||||
onClick={() => setFavorite(!favorite)}
|
><Icon icon="heart" />
|
||||||
>
|
|
||||||
<Icon icon="heart" />
|
|
||||||
</Button>
|
</Button>
|
||||||
|
{maybeRenderURL(performer.url ?? undefined)}
|
||||||
|
{/* TODO - render instagram and twitter links with icons */}
|
||||||
|
</span>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderNewView() {
|
||||||
|
return (
|
||||||
|
<div className="columns is-multiline no-spacing">
|
||||||
|
<div className="column is-half details-image-container">
|
||||||
|
<img className="performer" src={imagePreview} alt='' />
|
||||||
|
</div>
|
||||||
|
<div className="column is-half details-detail-container">
|
||||||
|
{renderTabs()}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const photos = [{src: imagePreview || "", caption: "Image"}];
|
||||||
|
|
||||||
|
function openLightbox() {
|
||||||
|
setLightboxIsOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLightbox() {
|
||||||
|
setLightboxIsOpen(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isNew) {
|
||||||
|
return renderNewView();
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div id="performer-page">
|
||||||
|
<div className="details-image-container">
|
||||||
|
<img className="performer" src={imagePreview} onClick={openLightbox} alt='' />
|
||||||
|
</div>
|
||||||
|
<div className="performer-head">
|
||||||
|
<h1 className="bp3-heading">
|
||||||
|
{performer.name}
|
||||||
|
{renderIcons()}
|
||||||
|
</h1>
|
||||||
|
{maybeRenderAliases()}
|
||||||
|
{maybeRenderAge()}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Table id="performer-details" style={{ width: "100%" }}>
|
<div className="performer-body">
|
||||||
<tbody>
|
<div className="details-detail-container">
|
||||||
{TableUtils.renderInputGroup({
|
{renderTabs()}
|
||||||
title: "Birthdate (YYYY-MM-DD)",
|
|
||||||
value: birthdate,
|
|
||||||
isEditing,
|
|
||||||
onChange: setBirthdate
|
|
||||||
})}
|
|
||||||
{renderEthnicity()}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Eye Color",
|
|
||||||
value: eyeColor,
|
|
||||||
isEditing,
|
|
||||||
onChange: setEyeColor
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Country",
|
|
||||||
value: country,
|
|
||||||
isEditing,
|
|
||||||
onChange: setCountry
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Height (CM)",
|
|
||||||
value: height,
|
|
||||||
isEditing,
|
|
||||||
onChange: setHeight
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Measurements",
|
|
||||||
value: measurements,
|
|
||||||
isEditing,
|
|
||||||
onChange: setMeasurements
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Fake Tits",
|
|
||||||
value: fakeTits,
|
|
||||||
isEditing,
|
|
||||||
onChange: setFakeTits
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Career Length",
|
|
||||||
value: careerLength,
|
|
||||||
isEditing,
|
|
||||||
onChange: setCareerLength
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Tattoos",
|
|
||||||
value: tattoos,
|
|
||||||
isEditing,
|
|
||||||
onChange: setTattoos
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Piercings",
|
|
||||||
value: piercings,
|
|
||||||
isEditing,
|
|
||||||
onChange: setPiercings
|
|
||||||
})}
|
|
||||||
{renderURLField()}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Twitter",
|
|
||||||
value: twitter,
|
|
||||||
isEditing,
|
|
||||||
onChange: setTwitter
|
|
||||||
})}
|
|
||||||
{TableUtils.renderInputGroup({
|
|
||||||
title: "Instagram",
|
|
||||||
value: instagram,
|
|
||||||
isEditing,
|
|
||||||
onChange: setInstagram
|
|
||||||
})}
|
|
||||||
</tbody>
|
|
||||||
</Table>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
<Lightbox
|
||||||
|
images={photos}
|
||||||
|
onClose={closeLightbox}
|
||||||
|
currentImage={0}
|
||||||
|
isOpen={lightboxIsOpen}
|
||||||
|
onClickImage={() => window.open(imagePreview, "_blank")}
|
||||||
|
width={9999}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,434 @@
|
|||||||
|
/* eslint-disable react/no-this-in-sfc */
|
||||||
|
|
||||||
|
import React, { useEffect, useState } from "react";
|
||||||
|
import { Button, Form, Popover, OverlayTrigger, Spinner, Table } from "react-bootstrap";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { StashService } from "src/core/StashService";
|
||||||
|
import {
|
||||||
|
Icon,
|
||||||
|
Modal,
|
||||||
|
ScrapePerformerSuggest
|
||||||
|
} from "src/components/Shared";
|
||||||
|
import { ImageUtils, TableUtils } from "src/utils";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
|
interface IPerformerDetails {
|
||||||
|
performer: Partial<GQL.PerformerDataFragment>;
|
||||||
|
isNew?: boolean;
|
||||||
|
isEditing?: boolean;
|
||||||
|
onSave?: (performer: Partial<GQL.PerformerCreateInput> | Partial<GQL.PerformerUpdateInput>) => void;
|
||||||
|
onDelete?: () => void;
|
||||||
|
onImageChange?: (image: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerformerDetailsPanel: React.FC<IPerformerDetails> = ({ performer, isNew, isEditing, onSave, onDelete, onImageChange }) => {
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
// Editing state
|
||||||
|
const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState<GQL.Scraper>();
|
||||||
|
const [scrapePerformerDetails, setScrapePerformerDetails] = useState<GQL.ScrapedPerformerDataFragment>();
|
||||||
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
||||||
|
// Editing performer state
|
||||||
|
const [image, setImage] = useState<string>();
|
||||||
|
const [name, setName] = useState<string>();
|
||||||
|
const [aliases, setAliases] = useState<string>();
|
||||||
|
const [favorite, setFavorite] = useState<boolean>();
|
||||||
|
const [birthdate, setBirthdate] = useState<string>();
|
||||||
|
const [ethnicity, setEthnicity] = useState<string>();
|
||||||
|
const [country, setCountry] = useState<string>();
|
||||||
|
const [eyeColor, setEyeColor] = useState<string>();
|
||||||
|
const [height, setHeight] = useState<string>();
|
||||||
|
const [measurements, setMeasurements] = useState<string>();
|
||||||
|
const [fakeTits, setFakeTits] = useState<string>();
|
||||||
|
const [careerLength, setCareerLength] = useState<string>();
|
||||||
|
const [tattoos, setTattoos] = useState<string>();
|
||||||
|
const [piercings, setPiercings] = useState<string>();
|
||||||
|
const [url, setUrl] = useState<string>();
|
||||||
|
const [twitter, setTwitter] = useState<string>();
|
||||||
|
const [instagram, setInstagram] = useState<string>();
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
|
const Scrapers = StashService.useListPerformerScrapers();
|
||||||
|
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||||
|
|
||||||
|
|
||||||
|
function updatePerformerEditState(
|
||||||
|
state: Partial<GQL.PerformerDataFragment | GQL.ScrapedPerformer>
|
||||||
|
) {
|
||||||
|
if ((state as GQL.PerformerDataFragment).favorite !== undefined) {
|
||||||
|
setFavorite((state as GQL.PerformerDataFragment).favorite);
|
||||||
|
}
|
||||||
|
setName(state.name ?? undefined);
|
||||||
|
setAliases(state.aliases ?? undefined);
|
||||||
|
setBirthdate(state.birthdate ?? undefined);
|
||||||
|
setEthnicity(state.ethnicity ?? undefined);
|
||||||
|
setCountry(state.country ?? undefined);
|
||||||
|
setEyeColor(state.eye_color ?? undefined);
|
||||||
|
setHeight(state.height ?? undefined);
|
||||||
|
setMeasurements(state.measurements ?? undefined);
|
||||||
|
setFakeTits(state.fake_tits ?? undefined);
|
||||||
|
setCareerLength(state.career_length ?? undefined);
|
||||||
|
setTattoos(state.tattoos ?? undefined);
|
||||||
|
setPiercings(state.piercings ?? undefined);
|
||||||
|
setUrl(state.url ?? undefined);
|
||||||
|
setTwitter(state.twitter ?? undefined);
|
||||||
|
setInstagram(state.instagram ?? undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImage(undefined);
|
||||||
|
updatePerformerEditState(performer);
|
||||||
|
}, [performer]);
|
||||||
|
|
||||||
|
function onImageLoad(this: FileReader) {
|
||||||
|
setImage(this.result as string);
|
||||||
|
if (onImageChange) {
|
||||||
|
onImageChange(this.result as string);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isEditing)
|
||||||
|
ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const newQueryableScrapers = (Scrapers?.data?.listPerformerScrapers ?? []).filter(s => (
|
||||||
|
s.performer?.supported_scrapes.includes(GQL.ScrapeType.Name)
|
||||||
|
));
|
||||||
|
|
||||||
|
setQueryableScrapers(newQueryableScrapers);
|
||||||
|
}, [Scrapers]);
|
||||||
|
|
||||||
|
if (isLoading)
|
||||||
|
return <Spinner animation="border" variant="light" />;
|
||||||
|
|
||||||
|
function getPerformerInput() {
|
||||||
|
const performerInput: Partial<
|
||||||
|
GQL.PerformerCreateInput | GQL.PerformerUpdateInput
|
||||||
|
> = {
|
||||||
|
name,
|
||||||
|
aliases,
|
||||||
|
favorite,
|
||||||
|
birthdate,
|
||||||
|
ethnicity,
|
||||||
|
country,
|
||||||
|
eye_color: eyeColor,
|
||||||
|
height,
|
||||||
|
measurements,
|
||||||
|
fake_tits: fakeTits,
|
||||||
|
career_length: careerLength,
|
||||||
|
tattoos,
|
||||||
|
piercings,
|
||||||
|
url,
|
||||||
|
twitter,
|
||||||
|
instagram,
|
||||||
|
image
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!isNew) {
|
||||||
|
(performerInput as GQL.PerformerUpdateInput).id = performer.id!;
|
||||||
|
}
|
||||||
|
return performerInput;
|
||||||
|
}
|
||||||
|
|
||||||
|
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
||||||
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
function onDisplayFreeOnesDialog(
|
||||||
|
scraper: GQL.Scraper
|
||||||
|
) {
|
||||||
|
setIsDisplayingScraperDialog(scraper);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getQueryScraperPerformerInput() {
|
||||||
|
if (!scrapePerformerDetails) return {};
|
||||||
|
|
||||||
|
const { __typename, ...ret } = scrapePerformerDetails;
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onScrapePerformer() {
|
||||||
|
setIsDisplayingScraperDialog(undefined);
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
if (!scrapePerformerDetails || !isDisplayingScraperDialog) return;
|
||||||
|
const result = await StashService.queryScrapePerformer(
|
||||||
|
isDisplayingScraperDialog.id,
|
||||||
|
getQueryScraperPerformerInput()
|
||||||
|
);
|
||||||
|
if (!result?.data?.scrapePerformer) return;
|
||||||
|
updatePerformerEditState(result.data.scrapePerformer);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onScrapePerformerURL() {
|
||||||
|
if (!url) return;
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
const result = await StashService.queryScrapePerformerURL(url);
|
||||||
|
if (!result.data || !result.data.scrapePerformerURL) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// leave URL as is if not set explicitly
|
||||||
|
if (!result.data.scrapePerformerURL.url) {
|
||||||
|
result.data.scrapePerformerURL.url = url;
|
||||||
|
}
|
||||||
|
updatePerformerEditState(result.data.scrapePerformerURL);
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderEthnicity() {
|
||||||
|
return TableUtils.renderHtmlSelect({
|
||||||
|
title: "Ethnicity",
|
||||||
|
value: ethnicity,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: (value: string) => setEthnicity(value),
|
||||||
|
selectOptions: ["white", "black", "asian", "hispanic"]
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScraperMenu() {
|
||||||
|
if (!performer || !isEditing) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const popover = (
|
||||||
|
<Popover id="scraper-popover">
|
||||||
|
<Popover.Content>
|
||||||
|
<div>
|
||||||
|
{queryableScrapers
|
||||||
|
? queryableScrapers.map(s => (
|
||||||
|
<Button
|
||||||
|
variant="link"
|
||||||
|
onClick={() =>
|
||||||
|
onDisplayFreeOnesDialog(s)
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{s.name}
|
||||||
|
</Button>
|
||||||
|
))
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</Popover.Content>
|
||||||
|
</Popover>
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<OverlayTrigger trigger="click" placement="bottom" overlay={popover}>
|
||||||
|
<Button>Scrape with...</Button>
|
||||||
|
</OverlayTrigger>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderScraperDialog() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={!!isDisplayingScraperDialog}
|
||||||
|
onHide={() => setIsDisplayingScraperDialog(undefined)}
|
||||||
|
header="Scrape"
|
||||||
|
accept={{ onClick: onScrapePerformer, text: "Scrape" }}
|
||||||
|
>
|
||||||
|
<div className="dialog-content">
|
||||||
|
<ScrapePerformerSuggest
|
||||||
|
placeholder="Performer name"
|
||||||
|
scraperId={
|
||||||
|
isDisplayingScraperDialog ? isDisplayingScraperDialog.id : ""
|
||||||
|
}
|
||||||
|
onSelectPerformer={query => setScrapePerformerDetails(query)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function urlScrapable(scrapedUrl: string) {
|
||||||
|
return (
|
||||||
|
!!scrapedUrl &&
|
||||||
|
(Scrapers?.data?.listPerformerScrapers ?? []).some(s =>
|
||||||
|
(s?.performer?.urls ?? []).some(u => scrapedUrl.includes(u))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderScrapeButton() {
|
||||||
|
if (!url || !isEditing || !urlScrapable(url)) {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<Button id="scrape-url-button" onClick={() => onScrapePerformerURL()}>
|
||||||
|
<Icon icon="file-upload" />
|
||||||
|
</Button>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderURLField() {
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td id="url-field">
|
||||||
|
URL
|
||||||
|
{maybeRenderScrapeButton()}
|
||||||
|
</td>
|
||||||
|
<td>
|
||||||
|
<Form.Control
|
||||||
|
value={url}
|
||||||
|
readOnly={!isEditing}
|
||||||
|
plaintext={!isEditing}
|
||||||
|
placeholder="URL"
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) =>
|
||||||
|
setUrl(event.currentTarget.value)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderButtons() {
|
||||||
|
if (isEditing) {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button className="edit-button" variant="primary" onClick={() => onSave?.(getPerformerInput())}>Save</Button>
|
||||||
|
{!isNew ? <Button className="edit-button" variant="danger" onClick={() => setIsDeleteAlertOpen(true)}>Delete</Button> : ''}
|
||||||
|
{renderScraperMenu()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function renderDeleteAlert() {
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
show={isDeleteAlertOpen}
|
||||||
|
icon="trash-alt"
|
||||||
|
accept={{ text: "Delete", variant: "danger", onClick: onDelete }}
|
||||||
|
cancel={{ onClick: () => setIsDeleteAlertOpen(false) }}
|
||||||
|
>
|
||||||
|
<p>
|
||||||
|
Are you sure you want to delete {name}?
|
||||||
|
</p>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderImageInput() {
|
||||||
|
if (!isEditing) { return; }
|
||||||
|
return (
|
||||||
|
<tr>
|
||||||
|
<td>Image</td>
|
||||||
|
<td><Form.Control type="file" onChange={onImageChangeHandler} accept=".jpg,.jpeg" /></td>
|
||||||
|
</tr>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderName() {
|
||||||
|
if (isEditing) {
|
||||||
|
return TableUtils.renderInputGroup(
|
||||||
|
{title: "Name", value: name, isEditing: !!isEditing, placeholder: "Name", onChange: setName});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function maybeRenderAliases() {
|
||||||
|
if (isEditing) {
|
||||||
|
return TableUtils.renderInputGroup(
|
||||||
|
{title: "Aliases", value: aliases, isEditing: !!isEditing, placeholder: "Aliases", onChange: setAliases});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
{renderDeleteAlert()}
|
||||||
|
{renderScraperDialog()}
|
||||||
|
|
||||||
|
<Table id="performer-details" style={{ width: "100%" }}>
|
||||||
|
<tbody>
|
||||||
|
{maybeRenderName()}
|
||||||
|
{maybeRenderAliases()}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Birthdate (YYYY-MM-DD)",
|
||||||
|
value: birthdate,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setBirthdate
|
||||||
|
})}
|
||||||
|
{renderEthnicity()}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Eye Color",
|
||||||
|
value: eyeColor,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setEyeColor
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Country",
|
||||||
|
value: country,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setCountry
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Height (CM)",
|
||||||
|
value: height,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setHeight
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Measurements",
|
||||||
|
value: measurements,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setMeasurements
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Fake Tits",
|
||||||
|
value: fakeTits,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setFakeTits
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Career Length",
|
||||||
|
value: careerLength,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setCareerLength
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Tattoos",
|
||||||
|
value: tattoos,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setTattoos
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Piercings",
|
||||||
|
value: piercings,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setPiercings
|
||||||
|
})}
|
||||||
|
{renderURLField()}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Twitter",
|
||||||
|
value: twitter,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setTwitter
|
||||||
|
})}
|
||||||
|
{TableUtils.renderInputGroup({
|
||||||
|
title: "Instagram",
|
||||||
|
value: instagram,
|
||||||
|
isEditing: !!isEditing,
|
||||||
|
onChange: setInstagram
|
||||||
|
})}
|
||||||
|
{renderImageInput()}
|
||||||
|
</tbody>
|
||||||
|
</Table>
|
||||||
|
|
||||||
|
{maybeRenderButtons()}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
import {
|
||||||
|
Button,
|
||||||
|
} from "react-bootstrap";
|
||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { StashService } from "src/core/StashService";
|
||||||
|
import { useToast } from "src/hooks";
|
||||||
|
|
||||||
|
interface IPerformerOperationsProps {
|
||||||
|
performer: Partial<GQL.PerformerDataFragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerformerOperationsPanel: React.FC<IPerformerOperationsProps> = ({ performer }) => {
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
|
async function onAutoTag() {
|
||||||
|
if (!performer?.id) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await StashService.queryMetadataAutoTag({ performers: [performer.id]});
|
||||||
|
Toast.success({ content: "Started auto tagging" });
|
||||||
|
} catch (e) {
|
||||||
|
Toast.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return <Button onClick={onAutoTag}>Auto Tag</Button>;
|
||||||
|
};
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import React from "react";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import { PerformersCriterion } from "src/models/list-filter/criteria/performers";
|
||||||
|
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||||
|
import { SceneList } from "../../scenes/SceneList";
|
||||||
|
|
||||||
|
interface IPerformerDetailsProps {
|
||||||
|
performer: Partial<GQL.PerformerDataFragment>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const PerformerScenesPanel: React.FC<IPerformerDetailsProps> = ({ performer }) => {
|
||||||
|
|
||||||
|
function filterHook(filter: ListFilterModel) {
|
||||||
|
const performerValue = {id: performer.id!, label: performer.name!};
|
||||||
|
// if performers is already present, then we modify it, otherwise add
|
||||||
|
let performerCriterion = filter.criteria.find((c) => {
|
||||||
|
return c.type === "performers";
|
||||||
|
});
|
||||||
|
|
||||||
|
if (performerCriterion &&
|
||||||
|
(performerCriterion.modifier === GQL.CriterionModifier.IncludesAll ||
|
||||||
|
performerCriterion.modifier === GQL.CriterionModifier.Includes)) {
|
||||||
|
// add the performer if not present
|
||||||
|
if (!performerCriterion.value.find((p : any) => {
|
||||||
|
return p.id === performer.id;
|
||||||
|
})) {
|
||||||
|
performerCriterion.value.push(performerValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
performerCriterion.modifier = GQL.CriterionModifier.IncludesAll;
|
||||||
|
} else {
|
||||||
|
// overwrite
|
||||||
|
performerCriterion = new PerformersCriterion();
|
||||||
|
performerCriterion.value = [performerValue];
|
||||||
|
filter.criteria.push(performerCriterion);
|
||||||
|
}
|
||||||
|
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<SceneList
|
||||||
|
subComponent
|
||||||
|
filterHook={filterHook}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -14,7 +14,12 @@ import { SceneCard } from "./SceneCard";
|
|||||||
import { SceneListTable } from "./SceneListTable";
|
import { SceneListTable } from "./SceneListTable";
|
||||||
import { SceneSelectedOptions } from "./SceneSelectedOptions";
|
import { SceneSelectedOptions } from "./SceneSelectedOptions";
|
||||||
|
|
||||||
export const SceneList: React.FC = () => {
|
interface ISceneList {
|
||||||
|
subComponent?: boolean;
|
||||||
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SceneList: React.FC<ISceneList> = ({ subComponent, filterHook }) => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const otherOperations = [
|
const otherOperations = [
|
||||||
{
|
{
|
||||||
@@ -27,7 +32,9 @@ export const SceneList: React.FC = () => {
|
|||||||
zoomable: true,
|
zoomable: true,
|
||||||
otherOperations,
|
otherOperations,
|
||||||
renderContent,
|
renderContent,
|
||||||
renderSelectedOptions
|
renderSelectedOptions,
|
||||||
|
subComponent,
|
||||||
|
filterHook
|
||||||
});
|
});
|
||||||
|
|
||||||
async function playRandom(
|
async function playRandom(
|
||||||
|
|||||||
@@ -290,27 +290,24 @@ export class StashService {
|
|||||||
"allPerformers"
|
"allPerformers"
|
||||||
];
|
];
|
||||||
|
|
||||||
public static usePerformerCreate(input: GQL.PerformerCreateInput) {
|
public static usePerformerCreate() {
|
||||||
return GQL.usePerformerCreateMutation({
|
return GQL.usePerformerCreateMutation({
|
||||||
variables: input,
|
|
||||||
update: () =>
|
update: () =>
|
||||||
StashService.invalidateQueries(
|
StashService.invalidateQueries(
|
||||||
StashService.performerMutationImpactedQueries
|
StashService.performerMutationImpactedQueries
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public static usePerformerUpdate(input: GQL.PerformerUpdateInput) {
|
public static usePerformerUpdate() {
|
||||||
return GQL.usePerformerUpdateMutation({
|
return GQL.usePerformerUpdateMutation({
|
||||||
variables: input,
|
|
||||||
update: () =>
|
update: () =>
|
||||||
StashService.invalidateQueries(
|
StashService.invalidateQueries(
|
||||||
StashService.performerMutationImpactedQueries
|
StashService.performerMutationImpactedQueries
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
public static usePerformerDestroy(input: GQL.PerformerDestroyInput) {
|
public static usePerformerDestroy() {
|
||||||
return GQL.usePerformerDestroyMutation({
|
return GQL.usePerformerDestroyMutation({
|
||||||
variables: input,
|
|
||||||
update: () =>
|
update: () =>
|
||||||
StashService.invalidateQueries(
|
StashService.invalidateQueries(
|
||||||
StashService.performerMutationImpactedQueries
|
StashService.performerMutationImpactedQueries
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ interface IListHookOperation<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface IListHookOptions<T> {
|
interface IListHookOptions<T> {
|
||||||
|
subComponent?: boolean;
|
||||||
|
filterHook?: (filter: ListFilterModel) => ListFilterModel;
|
||||||
zoomable?: boolean;
|
zoomable?: boolean;
|
||||||
otherOperations?: IListHookOperation<T>[];
|
otherOperations?: IListHookOperation<T>[];
|
||||||
renderContent: (
|
renderContent: (
|
||||||
@@ -75,17 +77,27 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
|||||||
const [filter, setFilter] = useState<ListFilterModel>(
|
const [filter, setFilter] = useState<ListFilterModel>(
|
||||||
new ListFilterModel(
|
new ListFilterModel(
|
||||||
options.filterMode,
|
options.filterMode,
|
||||||
queryString.parse(history.location.search)
|
options.subComponent ? '' : queryString.parse(history.location.search)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
|
||||||
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
|
||||||
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
const [zoomIndex, setZoomIndex] = useState<number>(1);
|
||||||
|
|
||||||
const result = options.useData(filter);
|
const result = options.useData(getFilter());
|
||||||
const totalCount = options.getCount(result);
|
const totalCount = options.getCount(result);
|
||||||
const items = options.getData(result);
|
const items = options.getData(result);
|
||||||
|
|
||||||
|
function getFilter() {
|
||||||
|
if (!options.filterHook) {
|
||||||
|
return filter;
|
||||||
|
}
|
||||||
|
|
||||||
|
// make a copy of the filter and call the hook
|
||||||
|
let newFilter = _.cloneDeep(filter);
|
||||||
|
return options.filterHook(newFilter);
|
||||||
|
}
|
||||||
|
|
||||||
function updateQueryParams(listfilter: ListFilterModel) {
|
function updateQueryParams(listfilter: ListFilterModel) {
|
||||||
const newLocation = { ...history.location };
|
const newLocation = { ...history.location };
|
||||||
newLocation.search = listfilter.makeQueryParameters();
|
newLocation.search = listfilter.makeQueryParameters();
|
||||||
|
|||||||
@@ -456,6 +456,39 @@ span.block {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#performer-page {
|
||||||
|
margin: 10px auto;
|
||||||
|
width: 75%;
|
||||||
|
|
||||||
|
& .details-image-container {
|
||||||
|
max-height: 400px;
|
||||||
|
display: inline-block;
|
||||||
|
margin-right: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .performer-head {
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: top;
|
||||||
|
font-size: 1.2em;
|
||||||
|
|
||||||
|
& .name-icons {
|
||||||
|
margin-left: 10px;
|
||||||
|
|
||||||
|
& .not-favorite .bp3-icon {
|
||||||
|
color: rgba(191, 204, 214, 0.5) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
& .favorite .bp3-icon {
|
||||||
|
color: #ff7373 !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
& .alias {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
.zoom-slider {
|
.zoom-slider {
|
||||||
margin: auto 5px;
|
margin: auto 5px;
|
||||||
width: 100px;
|
width: 100px;
|
||||||
|
|||||||
@@ -55,6 +55,8 @@ const renderInputGroup = (options: {
|
|||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
value: string | undefined;
|
value: string | undefined;
|
||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
|
asURL?: boolean,
|
||||||
|
urlPrefix?: string,
|
||||||
onChange: (value: string) => void;
|
onChange: (value: string) => void;
|
||||||
}) => (
|
}) => (
|
||||||
<tr>
|
<tr>
|
||||||
|
|||||||
Reference in New Issue
Block a user