Add scenes tab to performer page (#280)

This commit is contained in:
Infinite 2020-01-23 14:12:03 +01:00
parent dcfd445040
commit 63cc97d199
9 changed files with 720 additions and 368 deletions

View File

@ -1,18 +1,20 @@
/* eslint-disable react/no-this-in-sfc */
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 * as GQL from "src/core/generated-graphql";
import { StashService } from "src/core/StashService";
import {
DetailsEditNavbar,
Icon,
Modal,
ScrapePerformerSuggest
} 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 = () => {
const Toast = useToast();
@ -20,73 +22,19 @@ export const Performer: React.FC = () => {
const { id = "new" } = useParams();
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
const [performer, setPerformer] = useState<Partial<GQL.PerformerDataFragment>>({});
const [imagePreview, setImagePreview] = useState<string>();
const [lightboxIsOpen, setLightboxIsOpen] = useState(false);
// Network state
const [isLoading, setIsLoading] = useState(false);
const Scrapers = StashService.useListPerformerScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
const { data, error } = StashService.useFindPerformer(id);
const [updatePerformer] = StashService.usePerformerUpdate(
getPerformerInput() as GQL.PerformerUpdateInput
);
const [createPerformer] = StashService.usePerformerCreate(
getPerformerInput() as GQL.PerformerCreateInput
);
const [deletePerformer] = StashService.usePerformerDestroy(
getPerformerInput() as GQL.PerformerDestroyInput
);
const [updatePerformer] = StashService.usePerformerUpdate();
const [createPerformer] = StashService.usePerformerCreate();
const [deletePerformer] = StashService.usePerformerDestroy();
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(() => {
setIsLoading(false);
@ -95,69 +43,26 @@ export const Performer: React.FC = () => {
useEffect(() => {
setImagePreview(performer.image_path ?? undefined);
setImage(undefined);
updatePerformerEditState(performer);
if (!isNew)
setIsEditing(false);
}, [performer]);
function onImageLoad(this: FileReader) {
setImagePreview(this.result as string);
setImage(this.result as string);
function onImageChange(image: string) {
setImagePreview(image);
}
ImageUtils.usePasteImage(onImageLoad);
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)
if ((!isNew && (!data || !data.findPerformer)) || isLoading)
return <Spinner animation="border" variant="light" />;
if (error) return <div>{error.message}</div>;
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 = id;
}
return performerInput;
}
async function onSave() {
async function onSave(performerInput: Partial<GQL.PerformerCreateInput> | Partial<GQL.PerformerUpdateInput>) {
setIsLoading(true);
try {
if (!isNew) {
const result = await updatePerformer();
const result = await updatePerformer({variables: performerInput as GQL.PerformerUpdateInput});
if(result.data?.performerUpdate)
setPerformer(result.data?.performerUpdate);
} else {
const result = await createPerformer();
const result = await createPerformer({variables: performerInput as GQL.PerformerCreateInput});
if(result.data?.performerCreate) {
setPerformer(result.data.performerCreate);
history.push(`/performers/${result.data.performerCreate.id}`);
@ -172,7 +77,7 @@ export const Performer: React.FC = () => {
async function onDelete() {
setIsLoading(true);
try {
await deletePerformer();
await deletePerformer({variables: { id }});
} catch (e) {
Toast.error(e);
}
@ -182,273 +87,159 @@ export const Performer: React.FC = () => {
history.push("/performers");
}
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);
}
}
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()
function renderTabs() {
function renderEditPanel() {
return (
<PerformerDetailsPanel
performer={performer}
isEditing
isNew={isNew}
onDelete={onDelete}
onSave={onSave}
onImageChange={onImageChange}
/>
);
if (!result?.data?.scrapePerformer) return;
updatePerformerEditState(result.data.scrapePerformer);
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
// 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();
}
async function onScrapePerformerURL() {
if (!url) return;
setIsLoading(true);
try {
const result = await StashService.queryScrapePerformerURL(url);
if (!result.data || !result.data.scrapePerformerURL) {
return;
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>
</>
);
}
}
function maybeRenderAliases() {
if (performer && performer.aliases) {
return (
<>
<div>
<span className="alias-head">Also known as </span>
<span className="alias">{performer.aliases}</span>
</div>
</>
);
}
}
function setFavorite(v : boolean) {
performer.favorite = v;
onSave(performer);
}
function renderIcons() {
function maybeRenderURL(url?: string, icon: IconName = "link") {
if (performer.url) {
return (
<Button>
<a href={performer.url}>
<Icon icon={icon} />
</a>
</Button>
)
}
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 (
<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)}
/>
<>
<span className="name-icons">
<Button
className={performer.favorite ? "favorite" : "not-favorite"}
onClick={() => setFavorite(!performer.favorite)}
><Icon icon="heart" />
</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>
</Modal>
<div className="column is-half details-detail-container">
{renderTabs()}
</div>
</div>
);
}
function urlScrapable(scrapedUrl: string) {
return (
!!scrapedUrl &&
(Scrapers?.data?.listPerformerScrapers ?? []).some(s =>
(s?.performer?.urls ?? []).some(u => scrapedUrl.includes(u))
)
);
const photos = [{src: imagePreview || "", caption: "Image"}];
function openLightbox() {
setLightboxIsOpen(true);
}
function maybeRenderScrapeButton() {
if (!url || !isEditing || !urlScrapable(url)) {
return undefined;
}
return (
<Button id="scrape-url-button" onClick={() => onScrapePerformerURL()}>
<Icon icon="file-upload" />
</Button>
);
function closeLightbox() {
setLightboxIsOpen(false);
}
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>
);
if (isNew) {
return renderNewView();
}
return (
<>
{renderScraperDialog()}
<div className="row is-multiline no-spacing">
<div className="col-6 details-image-container">
<img className="performer" alt="" src={imagePreview} />
<div id="performer-page">
<div className="details-image-container">
<img className="performer" src={imagePreview} onClick={openLightbox} alt='' />
</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)}
/>
<div className="performer-head">
<h1 className="bp3-heading">
{performer.name}
{renderIcons()}
</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
disabled={!isEditing}
className={favorite ? "favorite" : undefined}
onClick={() => setFavorite(!favorite)}
>
<Icon icon="heart" />
</Button>
</div>
{maybeRenderAliases()}
{maybeRenderAge()}
</div>
<Table id="performer-details" style={{ width: "100%" }}>
<tbody>
{TableUtils.renderInputGroup({
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 className="performer-body">
<div className="details-detail-container">
{renderTabs()}
</div>
</div>
</div>
<Lightbox
images={photos}
onClose={closeLightbox}
currentImage={0}
isOpen={lightboxIsOpen}
onClickImage={() => window.open(imagePreview, "_blank")}
width={9999}
/>
</>
);
};

View File

@ -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()}
</>
);
};

View File

@ -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>;
};

View File

@ -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}
/>
);
}

View File

@ -14,7 +14,12 @@ import { SceneCard } from "./SceneCard";
import { SceneListTable } from "./SceneListTable";
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 otherOperations = [
{
@ -27,7 +32,9 @@ export const SceneList: React.FC = () => {
zoomable: true,
otherOperations,
renderContent,
renderSelectedOptions
renderSelectedOptions,
subComponent,
filterHook
});
async function playRandom(

View File

@ -290,27 +290,24 @@ export class StashService {
"allPerformers"
];
public static usePerformerCreate(input: GQL.PerformerCreateInput) {
public static usePerformerCreate() {
return GQL.usePerformerCreateMutation({
variables: input,
update: () =>
StashService.invalidateQueries(
StashService.performerMutationImpactedQueries
)
});
}
public static usePerformerUpdate(input: GQL.PerformerUpdateInput) {
public static usePerformerUpdate() {
return GQL.usePerformerUpdateMutation({
variables: input,
update: () =>
StashService.invalidateQueries(
StashService.performerMutationImpactedQueries
)
});
}
public static usePerformerDestroy(input: GQL.PerformerDestroyInput) {
public static usePerformerDestroy() {
return GQL.usePerformerDestroyMutation({
variables: input,
update: () =>
StashService.invalidateQueries(
StashService.performerMutationImpactedQueries

View File

@ -39,6 +39,8 @@ interface IListHookOperation<T> {
}
interface IListHookOptions<T> {
subComponent?: boolean;
filterHook?: (filter: ListFilterModel) => ListFilterModel;
zoomable?: boolean;
otherOperations?: IListHookOperation<T>[];
renderContent: (
@ -75,17 +77,27 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
const [filter, setFilter] = useState<ListFilterModel>(
new ListFilterModel(
options.filterMode,
queryString.parse(history.location.search)
options.subComponent ? '' : queryString.parse(history.location.search)
)
);
const [selectedIds, setSelectedIds] = useState<Set<string>>(new Set());
const [lastClickedId, setLastClickedId] = useState<string | undefined>();
const [zoomIndex, setZoomIndex] = useState<number>(1);
const result = options.useData(filter);
const result = options.useData(getFilter());
const totalCount = options.getCount(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) {
const newLocation = { ...history.location };
newLocation.search = listfilter.makeQueryParameters();

View File

@ -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 {
margin: auto 5px;
width: 100px;

View File

@ -55,6 +55,8 @@ const renderInputGroup = (options: {
placeholder?: string;
value: string | undefined;
isEditing: boolean;
asURL?: boolean,
urlPrefix?: string,
onChange: (value: string) => void;
}) => (
<tr>