mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Improve performer scrape search modal (#1198)
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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 {};
|
||||
async function onScrapePerformer(
|
||||
selectedPerformer: GQL.ScrapedPerformerDataFragment
|
||||
) {
|
||||
setScraper(undefined);
|
||||
try {
|
||||
if (!scraper) return;
|
||||
setIsLoading(true);
|
||||
|
||||
// image is not supported
|
||||
// remove tags as well
|
||||
const {
|
||||
__typename,
|
||||
image: _image,
|
||||
tags: _tags,
|
||||
...ret
|
||||
} = scrapePerformerDetails;
|
||||
return ret;
|
||||
}
|
||||
} = selectedPerformer;
|
||||
|
||||
async function onScrapePerformer() {
|
||||
setIsDisplayingScraperDialog(undefined);
|
||||
try {
|
||||
if (!scrapePerformerDetails || !isDisplayingScraperDialog) return;
|
||||
setIsLoading(true);
|
||||
const result = await queryScrapePerformer(
|
||||
isDisplayingScraperDialog.id,
|
||||
getQueryScraperPerformerInput()
|
||||
);
|
||||
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
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
Reference in New Issue
Block a user