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.
### 🎨 Improvements
* Improve performer scraper search modal.
* Add galleries tab to Tag details page.
* Allow scene/performer/studio image upload via URL.
* Add button to hide unmatched scenes in Tagger view.

View File

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

View File

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