Improve performer scrape search modal (#1198)

This commit is contained in:
InfiniteTF
2021-03-13 01:48:04 +01:00
committed by GitHub
parent b63e8ef929
commit a619b9dd48
6 changed files with 115 additions and 112 deletions

View File

@@ -2,6 +2,7 @@
* Added Performer tags. * Added Performer tags.
### 🎨 Improvements ### 🎨 Improvements
* Improve performer scraper search modal.
* Add galleries tab to Tag details page. * Add galleries tab to Tag details page.
* Allow scene/performer/studio image upload via URL. * Allow scene/performer/studio image upload via URL.
* Add button to hide unmatched scenes in Tagger view. * Add button to hide unmatched scenes in Tagger view.

View File

@@ -26,11 +26,10 @@ import {
} from "src/core/StashService"; } from "src/core/StashService";
import { import {
Icon, Icon,
Modal,
ImageInput, ImageInput,
ScrapePerformerSuggest,
LoadingIndicator, LoadingIndicator,
CollapseButton, CollapseButton,
Modal,
TagSelect, TagSelect,
} from "src/components/Shared"; } from "src/components/Shared";
import { ImageUtils } from "src/utils"; import { ImageUtils } from "src/utils";
@@ -38,6 +37,7 @@ import { useToast } from "src/hooks";
import { Prompt, useHistory } from "react-router-dom"; import { Prompt, useHistory } from "react-router-dom";
import { useFormik } from "formik"; import { useFormik } from "formik";
import { PerformerScrapeDialog } from "./PerformerScrapeDialog"; import { PerformerScrapeDialog } from "./PerformerScrapeDialog";
import PerformerScrapeModal from "./PerformerScrapeModal";
interface IPerformerDetails { interface IPerformerDetails {
performer: Partial<GQL.PerformerDataFragment>; performer: Partial<GQL.PerformerDataFragment>;
@@ -60,14 +60,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
const history = useHistory(); const history = useHistory();
// Editing state // Editing state
const [ const [scraper, setScraper] = useState<GQL.Scraper | undefined>();
isDisplayingScraperDialog,
setIsDisplayingScraperDialog,
] = useState<GQL.Scraper>();
const [
scrapePerformerDetails,
setScrapePerformerDetails,
] = useState<GQL.ScrapedPerformerDataFragment>();
const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>(); const [newTags, setNewTags] = useState<GQL.ScrapedSceneTag[]>();
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
@@ -422,10 +415,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
formik.setFieldValue("image", url); formik.setFieldValue("image", url);
} }
function onDisplayScrapeDialog(scraper: GQL.Scraper) {
setIsDisplayingScraperDialog(scraper);
}
async function onReloadScrapers() { async function onReloadScrapers() {
setIsLoading(true); setIsLoading(true);
try { try {
@@ -440,29 +429,22 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
} }
} }
function getQueryScraperPerformerInput() { async function onScrapePerformer(
if (!scrapePerformerDetails) return {}; selectedPerformer: GQL.ScrapedPerformerDataFragment
) {
setScraper(undefined);
try {
if (!scraper) return;
setIsLoading(true);
// image is not supported
// remove tags as well
const { const {
__typename, __typename,
image: _image, image: _image,
tags: _tags, tags: _tags,
...ret ...ret
} = scrapePerformerDetails; } = selectedPerformer;
return ret;
}
async function onScrapePerformer() { const result = await queryScrapePerformer(scraper.id, ret);
setIsDisplayingScraperDialog(undefined);
try {
if (!scrapePerformerDetails || !isDisplayingScraperDialog) return;
setIsLoading(true);
const result = await queryScrapePerformer(
isDisplayingScraperDialog.id,
getQueryScraperPerformerInput()
);
if (!result?.data?.scrapePerformer) return; if (!result?.data?.scrapePerformer) return;
// if this is a new performer, just dump the data // if this is a new performer, just dump the data
@@ -516,7 +498,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
<Button <Button
key={s.name} key={s.name}
className="minimal" className="minimal"
onClick={() => onDisplayScrapeDialog(s)} onClick={() => setScraper(s)}
> >
{s.name} {s.name}
</Button> </Button>
@@ -550,27 +532,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
); );
} }
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) { function urlScrapable(scrapedUrl?: string) {
return ( return (
!!scrapedUrl && !!scrapedUrl &&
@@ -662,6 +623,16 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
); );
} }
const renderScrapeModal = () =>
scraper !== undefined && (
<PerformerScrapeModal
scraper={scraper}
onHide={() => setScraper(undefined)}
onSelectPerformer={onScrapePerformer}
name={formik.values.name || ""}
/>
);
function renderDeleteAlert() { function renderDeleteAlert() {
return ( return (
<Modal <Modal
@@ -753,7 +724,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return ( return (
<> <>
{renderDeleteAlert()} {renderDeleteAlert()}
{renderScraperDialog()} {renderScrapeModal()}
{maybeRenderScrapeDialog()} {maybeRenderScrapeDialog()}
<Prompt <Prompt

View File

@@ -0,0 +1,71 @@
import React, { useEffect, useRef, useState } from "react";
import { debounce } from "lodash";
import { Button, Form } from "react-bootstrap";
import * as GQL from "src/core/generated-graphql";
import { Modal, LoadingIndicator } from "src/components/Shared";
import { useScrapePerformerList } from "src/core/StashService";
const CLASSNAME = "PerformerScrapeModal";
const CLASSNAME_LIST = `${CLASSNAME}-list`;
interface IProps {
scraper: GQL.Scraper;
onHide: () => void;
onSelectPerformer: (performer: GQL.ScrapedPerformerDataFragment) => void;
name?: string;
}
const PerformerScrapeModal: React.FC<IProps> = ({
scraper,
name,
onHide,
onSelectPerformer,
}) => {
const inputRef = useRef<HTMLInputElement>(null);
const [query, setQuery] = useState<string>(name ?? "");
const { data, loading } = useScrapePerformerList(scraper.id, query);
const performers = data?.scrapePerformerList ?? [];
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
useEffect(() => inputRef.current?.focus(), []);
return (
<Modal
show
onHide={onHide}
header={`Scrape performer from ${scraper.name}`}
accept={{ text: "Cancel", onClick: onHide, variant: "secondary" }}
>
<div className={CLASSNAME}>
<Form.Control
onChange={(e) => onInputChange(e.currentTarget.value)}
defaultValue={name ?? ""}
placeholder="Performer name..."
className="text-input mb-4"
ref={inputRef}
/>
{loading ? (
<div className="m-4 text-center">
<LoadingIndicator inline />
</div>
) : (
<ul className={CLASSNAME_LIST}>
{performers.map((p) => (
<li key={p.url}>
<Button variant="link" onClick={() => onSelectPerformer(p)}>
{p.name}
</Button>
</li>
))}
</ul>
)}
</div>
</Modal>
);
};
export default PerformerScrapeModal;

View File

@@ -90,3 +90,16 @@
#performer-scraper-popover { #performer-scraper-popover {
z-index: 1; z-index: 1;
} }
.PerformerScrapeModal {
&-list {
list-style-type: none;
max-height: 50vh;
overflow-x: auto;
padding-left: 1rem;
.btn {
font-size: 1.2rem;
}
}
}

View File

@@ -10,7 +10,6 @@ import {
useAllStudiosForFilter, useAllStudiosForFilter,
useAllPerformersForFilter, useAllPerformersForFilter,
useMarkerStrings, useMarkerStrings,
useScrapePerformerList,
useTagCreate, useTagCreate,
useStudioCreate, useStudioCreate,
usePerformerCreate, usePerformerCreate,
@@ -342,48 +341,6 @@ export const SceneSelect: React.FC<ISceneSelect> = (props) => {
); );
}; };
interface IScrapePerformerSuggestProps {
scraperId: string;
onSelectPerformer: (performer: GQL.ScrapedPerformerDataFragment) => void;
placeholder?: string;
}
export const ScrapePerformerSuggest: React.FC<IScrapePerformerSuggestProps> = (
props
) => {
const [query, setQuery] = useState<string>("");
const { data, loading } = useScrapePerformerList(props.scraperId, query);
const performers = data?.scrapePerformerList ?? [];
const items = performers.map((item) => ({
label: item.name ?? "",
value: item.name ?? "",
}));
const onInputChange = debounce((input: string) => {
setQuery(input);
}, 500);
const onChange = (option: ValueType<Option, false>) => {
const performer = performers.find((p) => p.name === option?.value);
if (performer) props.onSelectPerformer(performer);
};
return (
<SelectComponent
isMulti={false}
onChange={onChange}
onInputChange={onInputChange}
isLoading={loading}
items={items}
initialIds={[]}
placeholder={props.placeholder}
className="select-suggest"
showDropdown={false}
noOptionsMessage={query === "" ? null : "No performers found."}
/>
);
};
interface IMarkerSuggestProps { interface IMarkerSuggestProps {
initialMarkerTitle?: string; initialMarkerTitle?: string;
onChange: (title: string) => void; onChange: (title: string) => void;

View File

@@ -1,14 +1,4 @@
export { export * from "./Select";
GallerySelect,
ScrapePerformerSuggest,
MarkerTitleSuggest,
FilterSelect,
PerformerSelect,
StudioSelect,
TagSelect,
SceneSelect,
} from "./Select";
export { default as Icon } from "./Icon"; export { default as Icon } from "./Icon";
export { default as Modal } from "./Modal"; export { default as Modal } from "./Modal";
export { CollapseButton } from "./CollapseButton"; export { CollapseButton } from "./CollapseButton";