From 63cc97d1990eae3bfcfd878a5fa38b0e5c546513 Mon Sep 17 00:00:00 2001 From: Infinite Date: Thu, 23 Jan 2020 14:12:03 +0100 Subject: [PATCH] Add scenes tab to performer page (#280) --- .../performers/PerformerDetails/Performer.tsx | 507 +++++------------- .../PerformerDetailsPanel.tsx | 434 +++++++++++++++ .../PerformerOperationsPanel.tsx | 29 + .../PerformerDetails/PerformerScenesPanel.tsx | 47 ++ ui/v2.5/src/components/scenes/SceneList.tsx | 11 +- ui/v2.5/src/core/StashService.ts | 9 +- ui/v2.5/src/hooks/ListHook.tsx | 16 +- ui/v2.5/src/index.scss | 33 ++ ui/v2.5/src/utils/table.tsx | 2 + 9 files changed, 720 insertions(+), 368 deletions(-) create mode 100644 ui/v2.5/src/components/performers/PerformerDetails/PerformerDetailsPanel.tsx create mode 100644 ui/v2.5/src/components/performers/PerformerDetails/PerformerOperationsPanel.tsx create mode 100644 ui/v2.5/src/components/performers/PerformerDetails/PerformerScenesPanel.tsx diff --git a/ui/v2.5/src/components/performers/PerformerDetails/Performer.tsx b/ui/v2.5/src/components/performers/PerformerDetails/Performer.tsx index 3bce41ed9..1a8b55788 100644 --- a/ui/v2.5/src/components/performers/PerformerDetails/Performer.tsx +++ b/ui/v2.5/src/components/performers/PerformerDetails/Performer.tsx @@ -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(isNew); - const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState(); - const [scrapePerformerDetails, setScrapePerformerDetails] = useState(); - - // Editing performer state - const [image, setImage] = useState(); - const [name, setName] = useState(); - const [aliases, setAliases] = useState(); - const [favorite, setFavorite] = useState(); - const [birthdate, setBirthdate] = useState(); - const [ethnicity, setEthnicity] = useState(); - const [country, setCountry] = useState(); - const [eyeColor, setEyeColor] = useState(); - const [height, setHeight] = useState(); - const [measurements, setMeasurements] = useState(); - const [fakeTits, setFakeTits] = useState(); - const [careerLength, setCareerLength] = useState(); - const [tattoos, setTattoos] = useState(); - const [piercings, setPiercings] = useState(); - const [url, setUrl] = useState(); - const [twitter, setTwitter] = useState(); - const [instagram, setInstagram] = useState(); - // Performer state const [performer, setPerformer] = useState>({}); const [imagePreview, setImagePreview] = useState(); + const [lightboxIsOpen, setLightboxIsOpen] = useState(false); // Network state const [isLoading, setIsLoading] = useState(false); - const Scrapers = StashService.useListPerformerScrapers(); - const [queryableScrapers, setQueryableScrapers] = useState([]); - 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 - ) { - 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 ; + if (error) return
{error.message}
; - 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 | Partial) { 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) { - 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 ( + ); - if (!result?.data?.scrapePerformer) return; - updatePerformerEditState(result.data.scrapePerformer); - } catch (e) { - Toast.error(e); } - setIsLoading(false); + + // render tabs if not new + if (!isNew) { + return ( + + + + + + + + + { renderEditPanel() } + + + + + + ); + } + 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 ( + <> +
+ {TextUtils.age(performer.birthdate)} + years old +
+ + ); + } + } + + function maybeRenderAliases() { + if (performer && performer.aliases) { + return ( + <> +
+ Also known as + {performer.aliases} +
+ + ); + } + } + + function setFavorite(v : boolean) { + performer.favorite = v; + onSave(performer); + } + + function renderIcons() { + function maybeRenderURL(url?: string, icon: IconName = "link") { + if (performer.url) { + 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 ( - setIsDisplayingScraperDialog(undefined)} - header="Scrape" - accept={{ onClick: onScrapePerformer, text: "Scrape" }} - > -
- setScrapePerformerDetails(query)} - /> + <> + + + {maybeRenderURL(performer.url ?? undefined)} + {/* TODO - render instagram and twitter links with icons */} + + + ); + } + + function renderNewView() { + return ( +
+
+
- +
+ {renderTabs()} +
+
); } - 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 ( - - ); + function closeLightbox() { + setLightboxIsOpen(false); } - function renderURLField() { - return ( - - - URL - {maybeRenderScrapeButton()} - - - ) => - setUrl(event.currentTarget.value) - } - /> - - - ); + if (isNew) { + return renderNewView(); } return ( <> - {renderScraperDialog()} -
-
- +
+
+
-
- { - setIsEditing(!isEditing); - updatePerformerEditState(performer); - }} - onSave={onSave} - onDelete={onDelete} - onImageChange={onImageChangeHandler} - scrapers={queryableScrapers} - onDisplayScraperDialog={onDisplayFreeOnesDialog} - onAutoTag={onAutoTag} - /> -

- setName(event.target.value)} - /> +
+

+ {performer.name} + {renderIcons()}

-
- - Aliases: - ) => - setAliases(event.currentTarget.value) - } - /> - -
-
- Favorite: - -
+ {maybeRenderAliases()} + {maybeRenderAge()} +
- - - {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 - })} - -
+
+
+ {renderTabs()} +

+ window.open(imagePreview, "_blank")} + width={9999} + /> ); }; diff --git a/ui/v2.5/src/components/performers/PerformerDetails/PerformerDetailsPanel.tsx b/ui/v2.5/src/components/performers/PerformerDetails/PerformerDetailsPanel.tsx new file mode 100644 index 000000000..19913a5fc --- /dev/null +++ b/ui/v2.5/src/components/performers/PerformerDetails/PerformerDetailsPanel.tsx @@ -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; + isNew?: boolean; + isEditing?: boolean; + onSave?: (performer: Partial | Partial) => void; + onDelete?: () => void; + onImageChange?: (image: string) => void; +} + +export const PerformerDetailsPanel: React.FC = ({ performer, isNew, isEditing, onSave, onDelete, onImageChange }) => { + const Toast = useToast(); + + // Editing state + const [isDisplayingScraperDialog, setIsDisplayingScraperDialog] = useState(); + const [scrapePerformerDetails, setScrapePerformerDetails] = useState(); + const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + + + // Editing performer state + const [image, setImage] = useState(); + const [name, setName] = useState(); + const [aliases, setAliases] = useState(); + const [favorite, setFavorite] = useState(); + const [birthdate, setBirthdate] = useState(); + const [ethnicity, setEthnicity] = useState(); + const [country, setCountry] = useState(); + const [eyeColor, setEyeColor] = useState(); + const [height, setHeight] = useState(); + const [measurements, setMeasurements] = useState(); + const [fakeTits, setFakeTits] = useState(); + const [careerLength, setCareerLength] = useState(); + const [tattoos, setTattoos] = useState(); + const [piercings, setPiercings] = useState(); + const [url, setUrl] = useState(); + const [twitter, setTwitter] = useState(); + const [instagram, setInstagram] = useState(); + + // Network state + const [isLoading, setIsLoading] = useState(false); + + const Scrapers = StashService.useListPerformerScrapers(); + const [queryableScrapers, setQueryableScrapers] = useState([]); + + + function updatePerformerEditState( + state: Partial + ) { + 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 ; + + 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) { + 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 = ( + + +
+ {queryableScrapers + ? queryableScrapers.map(s => ( + + )) + : ""} +
+
+
+ ); + + return ( + + + + ); + } + + function renderScraperDialog() { + return ( + setIsDisplayingScraperDialog(undefined)} + header="Scrape" + accept={{ onClick: onScrapePerformer, text: "Scrape" }} + > +
+ setScrapePerformerDetails(query)} + /> +
+
+ ); + } + + 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 ( + + ); + } + + function renderURLField() { + return ( + + + URL + {maybeRenderScrapeButton()} + + + ) => + setUrl(event.currentTarget.value) + } + /> + + + ); + } + + function maybeRenderButtons() { + if (isEditing) { + return ( + <> + + {!isNew ? : ''} + {renderScraperMenu()} + + ); + } + } + + + function renderDeleteAlert() { + return ( + setIsDeleteAlertOpen(false) }} + > +

+ Are you sure you want to delete {name}? +

+
+ ); + } + + function renderImageInput() { + if (!isEditing) { return; } + return ( + + Image + + + ) + } + + 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()} + + + + {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()} + +
+ + {maybeRenderButtons()} + + ); +}; diff --git a/ui/v2.5/src/components/performers/PerformerDetails/PerformerOperationsPanel.tsx b/ui/v2.5/src/components/performers/PerformerDetails/PerformerOperationsPanel.tsx new file mode 100644 index 000000000..0d5603016 --- /dev/null +++ b/ui/v2.5/src/components/performers/PerformerDetails/PerformerOperationsPanel.tsx @@ -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 +} + +export const PerformerOperationsPanel: React.FC = ({ 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 ; +}; diff --git a/ui/v2.5/src/components/performers/PerformerDetails/PerformerScenesPanel.tsx b/ui/v2.5/src/components/performers/PerformerDetails/PerformerScenesPanel.tsx new file mode 100644 index 000000000..b8a4f24b0 --- /dev/null +++ b/ui/v2.5/src/components/performers/PerformerDetails/PerformerScenesPanel.tsx @@ -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 +} + +export const PerformerScenesPanel: React.FC = ({ 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 ( + + ); +} diff --git a/ui/v2.5/src/components/scenes/SceneList.tsx b/ui/v2.5/src/components/scenes/SceneList.tsx index aa599aff2..1a04e562c 100644 --- a/ui/v2.5/src/components/scenes/SceneList.tsx +++ b/ui/v2.5/src/components/scenes/SceneList.tsx @@ -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 = ({ 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( diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 3e54b5611..3be5b544c 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -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 diff --git a/ui/v2.5/src/hooks/ListHook.tsx b/ui/v2.5/src/hooks/ListHook.tsx index af1d60fcf..8d13c7073 100644 --- a/ui/v2.5/src/hooks/ListHook.tsx +++ b/ui/v2.5/src/hooks/ListHook.tsx @@ -39,6 +39,8 @@ interface IListHookOperation { } interface IListHookOptions { + subComponent?: boolean; + filterHook?: (filter: ListFilterModel) => ListFilterModel; zoomable?: boolean; otherOperations?: IListHookOperation[]; renderContent: ( @@ -75,17 +77,27 @@ const useList = ( const [filter, setFilter] = useState( new ListFilterModel( options.filterMode, - queryString.parse(history.location.search) + options.subComponent ? '' : queryString.parse(history.location.search) ) ); const [selectedIds, setSelectedIds] = useState>(new Set()); const [lastClickedId, setLastClickedId] = useState(); const [zoomIndex, setZoomIndex] = useState(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(); diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 73f534f4a..6059816c3 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -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; diff --git a/ui/v2.5/src/utils/table.tsx b/ui/v2.5/src/utils/table.tsx index 5a1834abd..2176db8f4 100644 --- a/ui/v2.5/src/utils/table.tsx +++ b/ui/v2.5/src/utils/table.tsx @@ -55,6 +55,8 @@ const renderInputGroup = (options: { placeholder?: string; value: string | undefined; isEditing: boolean; + asURL?: boolean, + urlPrefix?: string, onChange: (value: string) => void; }) => (