mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
Scraper menu filter (#5041)
* Move scene scraper menu into reusable component * Reuse ScraperMenu for scene query menu * Reuse scraper menu in GalleryEditPanel * Add filter to scraper menu * Add divider between stashboxes and scrapers
This commit is contained in:
@@ -1,14 +1,7 @@
|
|||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import {
|
import { Button, Form, Col, Row } from "react-bootstrap";
|
||||||
Button,
|
|
||||||
Dropdown,
|
|
||||||
DropdownButton,
|
|
||||||
Form,
|
|
||||||
Col,
|
|
||||||
Row,
|
|
||||||
} from "react-bootstrap";
|
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -18,12 +11,10 @@ import {
|
|||||||
useListGalleryScrapers,
|
useListGalleryScrapers,
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
import { GalleryScrapeDialog } from "./GalleryScrapeDialog";
|
||||||
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
import { handleUnsavedChanges } from "src/utils/navigation";
|
import { handleUnsavedChanges } from "src/utils/navigation";
|
||||||
import {
|
import {
|
||||||
@@ -39,6 +30,7 @@ import { formikUtils } from "src/utils/form";
|
|||||||
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
||||||
import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect";
|
import { Scene, SceneSelect } from "src/components/Scenes/SceneSelect";
|
||||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||||
|
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
@@ -62,8 +54,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const isNew = gallery.id === undefined;
|
const isNew = gallery.id === undefined;
|
||||||
|
|
||||||
const Scrapers = useListGalleryScrapers();
|
const scrapers = useListGalleryScrapers();
|
||||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
|
||||||
|
|
||||||
const [scrapedGallery, setScrapedGallery] =
|
const [scrapedGallery, setScrapedGallery] =
|
||||||
useState<GQL.ScrapedGallery | null>();
|
useState<GQL.ScrapedGallery | null>();
|
||||||
@@ -165,13 +156,11 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
const fragmentScrapers = useMemo(() => {
|
||||||
const newQueryableScrapers = (Scrapers?.data?.listScrapers ?? []).filter(
|
return (scrapers?.data?.listScrapers ?? []).filter((s) =>
|
||||||
(s) => s.gallery?.supported_scrapes.includes(GQL.ScrapeType.Fragment)
|
s.gallery?.supported_scrapes.includes(GQL.ScrapeType.Fragment)
|
||||||
);
|
);
|
||||||
|
}, [scrapers]);
|
||||||
setQueryableScrapers(newQueryableScrapers);
|
|
||||||
}, [Scrapers]);
|
|
||||||
|
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
@@ -184,12 +173,12 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapeClicked(scraper: GQL.Scraper) {
|
async function onScrapeClicked(s: GQL.ScraperSourceInput) {
|
||||||
if (!gallery || !gallery.id) return;
|
if (!gallery || !gallery.id) return;
|
||||||
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await queryScrapeGallery(scraper.id, gallery.id);
|
const result = await queryScrapeGallery(s.scraper_id!, gallery.id);
|
||||||
if (!result.data || !result.data.scrapeSingleGallery?.length) {
|
if (!result.data || !result.data.scrapeSingleGallery?.length) {
|
||||||
Toast.success("No galleries found");
|
Toast.success("No galleries found");
|
||||||
return;
|
return;
|
||||||
@@ -244,36 +233,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderScraperMenu() {
|
|
||||||
if (isNew) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownButton
|
|
||||||
className="d-inline-block"
|
|
||||||
id="gallery-scrape"
|
|
||||||
title={intl.formatMessage({ id: "actions.scrape_with" })}
|
|
||||||
>
|
|
||||||
{queryableScrapers.map((s) => (
|
|
||||||
<Dropdown.Item key={s.name} onClick={() => onScrapeClicked(s)}>
|
|
||||||
{s.name}
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
<Dropdown.Item onClick={() => onReloadScrapers()}>
|
|
||||||
<span className="fa-icon">
|
|
||||||
<Icon icon={faSyncAlt} />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<FormattedMessage id="actions.reload_scrapers" />
|
|
||||||
</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
</DropdownButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function urlScrapable(scrapedUrl: string): boolean {
|
function urlScrapable(scrapedUrl: string): boolean {
|
||||||
return (Scrapers?.data?.listScrapers ?? []).some((s) =>
|
return (scrapers?.data?.listScrapers ?? []).some((s) =>
|
||||||
(s?.gallery?.urls ?? []).some((u) => scrapedUrl.includes(u))
|
(s?.gallery?.urls ?? []).some((u) => scrapedUrl.includes(u))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -461,7 +422,16 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
<FormattedMessage id="actions.delete" />
|
<FormattedMessage id="actions.delete" />
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<div className="ml-auto text-right d-flex">{renderScraperMenu()}</div>
|
<div className="ml-auto text-right d-flex">
|
||||||
|
{!isNew && (
|
||||||
|
<ScraperMenu
|
||||||
|
toggle={intl.formatMessage({ id: "actions.scrape_with" })}
|
||||||
|
scrapers={fragmentScrapers}
|
||||||
|
onScraperClicked={onScrapeClicked}
|
||||||
|
onReloadScrapers={onReloadScrapers}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</Row>
|
</Row>
|
||||||
<Row className="form-container px-3">
|
<Row className="form-container px-3">
|
||||||
<Col lg={7} xl={12}>
|
<Col lg={7} xl={12}>
|
||||||
|
|||||||
@@ -1,14 +1,6 @@
|
|||||||
import React, { useEffect, useState, useMemo } from "react";
|
import React, { useEffect, useState, useMemo } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import {
|
import { Button, Form, Col, Row, ButtonGroup } from "react-bootstrap";
|
||||||
Button,
|
|
||||||
Dropdown,
|
|
||||||
DropdownButton,
|
|
||||||
Form,
|
|
||||||
Col,
|
|
||||||
Row,
|
|
||||||
ButtonGroup,
|
|
||||||
} from "react-bootstrap";
|
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
@@ -28,9 +20,8 @@ import { getStashIDs } from "src/utils/stashIds";
|
|||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
|
||||||
import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable";
|
import { IGroupEntry, SceneGroupTable } from "./SceneGroupTable";
|
||||||
import { faSearch, faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
import { faSearch } from "@fortawesome/free-solid-svg-icons";
|
||||||
import { objectTitle } from "src/core/files";
|
import { objectTitle } from "src/core/files";
|
||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
import { lazyComponent } from "src/utils/lazyComponent";
|
import { lazyComponent } from "src/utils/lazyComponent";
|
||||||
@@ -49,6 +40,7 @@ import { Studio, StudioSelect } from "src/components/Studios/StudioSelect";
|
|||||||
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
|
import { Gallery, GallerySelect } from "src/components/Galleries/GallerySelect";
|
||||||
import { Group } from "src/components/Groups/GroupSelect";
|
import { Group } from "src/components/Groups/GroupSelect";
|
||||||
import { useTagsEdit } from "src/hooks/tagsEdit";
|
import { useTagsEdit } from "src/hooks/tagsEdit";
|
||||||
|
import { ScraperMenu } from "src/components/Shared/ScraperMenu";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
@@ -394,51 +386,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderScrapeQueryMenu() {
|
|
||||||
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
|
|
||||||
|
|
||||||
if (stashBoxes.length === 0 && queryableScrapers.length === 0) return;
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dropdown title={intl.formatMessage({ id: "actions.scrape_query" })}>
|
|
||||||
<Dropdown.Toggle variant="secondary">
|
|
||||||
<Icon icon={faSearch} />
|
|
||||||
</Dropdown.Toggle>
|
|
||||||
|
|
||||||
<Dropdown.Menu>
|
|
||||||
{stashBoxes.map((s, index) => (
|
|
||||||
<Dropdown.Item
|
|
||||||
key={s.endpoint}
|
|
||||||
onClick={() =>
|
|
||||||
onScrapeQueryClicked({
|
|
||||||
stash_box_endpoint: s.endpoint,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{stashboxDisplayName(s.name, index)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
{queryableScrapers.map((s) => (
|
|
||||||
<Dropdown.Item
|
|
||||||
key={s.name}
|
|
||||||
onClick={() => onScrapeQueryClicked({ scraper_id: s.id })}
|
|
||||||
>
|
|
||||||
{s.name}
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
<Dropdown.Item onClick={() => onReloadScrapers()}>
|
|
||||||
<span className="fa-icon">
|
|
||||||
<Icon icon={faSyncAlt} />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<FormattedMessage id="actions.reload_scrapers" />
|
|
||||||
</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
</Dropdown.Menu>
|
|
||||||
</Dropdown>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onSceneSelected(s: GQL.ScrapedSceneDataFragment) {
|
function onSceneSelected(s: GQL.ScrapedSceneDataFragment) {
|
||||||
if (!scraper) return;
|
if (!scraper) return;
|
||||||
|
|
||||||
@@ -468,47 +415,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderScraperMenu() {
|
|
||||||
const stashBoxes = stashConfig?.general.stashBoxes ?? [];
|
|
||||||
|
|
||||||
return (
|
|
||||||
<DropdownButton
|
|
||||||
className="d-inline-block"
|
|
||||||
id="scene-scrape"
|
|
||||||
title={intl.formatMessage({ id: "actions.scrape_with" })}
|
|
||||||
>
|
|
||||||
{stashBoxes.map((s, index) => (
|
|
||||||
<Dropdown.Item
|
|
||||||
key={s.endpoint}
|
|
||||||
onClick={() =>
|
|
||||||
onScrapeClicked({
|
|
||||||
stash_box_endpoint: s.endpoint,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{stashboxDisplayName(s.name, index)}
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
{fragmentScrapers.map((s) => (
|
|
||||||
<Dropdown.Item
|
|
||||||
key={s.name}
|
|
||||||
onClick={() => onScrapeClicked({ scraper_id: s.id })}
|
|
||||||
>
|
|
||||||
{s.name}
|
|
||||||
</Dropdown.Item>
|
|
||||||
))}
|
|
||||||
<Dropdown.Item onClick={() => onReloadScrapers()}>
|
|
||||||
<span className="fa-icon">
|
|
||||||
<Icon icon={faSyncAlt} />
|
|
||||||
</span>
|
|
||||||
<span>
|
|
||||||
<FormattedMessage id="actions.reload_scrapers" />
|
|
||||||
</span>
|
|
||||||
</Dropdown.Item>
|
|
||||||
</DropdownButton>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function urlScrapable(scrapedUrl: string): boolean {
|
function urlScrapable(scrapedUrl: string): boolean {
|
||||||
return (Scrapers?.data?.listScrapers ?? []).some((s) =>
|
return (Scrapers?.data?.listScrapers ?? []).some((s) =>
|
||||||
(s?.scene?.urls ?? []).some((u) => scrapedUrl.includes(u))
|
(s?.scene?.urls ?? []).some((u) => scrapedUrl.includes(u))
|
||||||
@@ -801,8 +707,21 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
{!isNew && (
|
{!isNew && (
|
||||||
<div className="ml-auto text-right d-flex">
|
<div className="ml-auto text-right d-flex">
|
||||||
<ButtonGroup className="scraper-group">
|
<ButtonGroup className="scraper-group">
|
||||||
{renderScraperMenu()}
|
<ScraperMenu
|
||||||
{renderScrapeQueryMenu()}
|
toggle={intl.formatMessage({ id: "actions.scrape_with" })}
|
||||||
|
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
||||||
|
scrapers={fragmentScrapers}
|
||||||
|
onScraperClicked={onScrapeClicked}
|
||||||
|
onReloadScrapers={onReloadScrapers}
|
||||||
|
/>
|
||||||
|
<ScraperMenu
|
||||||
|
variant="secondary"
|
||||||
|
toggle={<Icon icon={faSearch} />}
|
||||||
|
stashBoxes={stashConfig?.general.stashBoxes ?? []}
|
||||||
|
scrapers={queryableScrapers}
|
||||||
|
onScraperClicked={onScrapeQueryClicked}
|
||||||
|
onReloadScrapers={onReloadScrapers}
|
||||||
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
100
ui/v2.5/src/components/Shared/ScraperMenu.tsx
Normal file
100
ui/v2.5/src/components/Shared/ScraperMenu.tsx
Normal file
@@ -0,0 +1,100 @@
|
|||||||
|
import React, { useMemo, useState } from "react";
|
||||||
|
import { Dropdown } from "react-bootstrap";
|
||||||
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
|
import { Icon } from "./Icon";
|
||||||
|
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||||
|
import { ScraperSourceInput, StashBox } from "src/core/generated-graphql";
|
||||||
|
import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
||||||
|
import { ClearableInput } from "./ClearableInput";
|
||||||
|
|
||||||
|
const minFilteredScrapers = 5;
|
||||||
|
|
||||||
|
export const ScraperMenu: React.FC<{
|
||||||
|
toggle: React.ReactNode;
|
||||||
|
variant?: string;
|
||||||
|
stashBoxes?: StashBox[];
|
||||||
|
scrapers: { id: string; name: string }[];
|
||||||
|
onScraperClicked: (s: ScraperSourceInput) => void;
|
||||||
|
onReloadScrapers: () => void;
|
||||||
|
}> = ({
|
||||||
|
toggle,
|
||||||
|
variant,
|
||||||
|
stashBoxes,
|
||||||
|
scrapers,
|
||||||
|
onScraperClicked,
|
||||||
|
onReloadScrapers,
|
||||||
|
}) => {
|
||||||
|
const intl = useIntl();
|
||||||
|
const [filter, setFilter] = useState("");
|
||||||
|
|
||||||
|
const filteredStashboxes = useMemo(() => {
|
||||||
|
if (!stashBoxes) return [];
|
||||||
|
if (!filter) return stashBoxes;
|
||||||
|
|
||||||
|
return stashBoxes.filter((s) =>
|
||||||
|
s.name.toLowerCase().includes(filter.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [stashBoxes, filter]);
|
||||||
|
|
||||||
|
const filteredScrapers = useMemo(() => {
|
||||||
|
if (!filter) return scrapers;
|
||||||
|
|
||||||
|
return scrapers.filter(
|
||||||
|
(s) =>
|
||||||
|
s.name.toLowerCase().includes(filter.toLowerCase()) ||
|
||||||
|
s.id.toLowerCase().includes(filter.toLowerCase())
|
||||||
|
);
|
||||||
|
}, [scrapers, filter]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dropdown
|
||||||
|
className="scraper-menu"
|
||||||
|
title={intl.formatMessage({ id: "actions.scrape_query" })}
|
||||||
|
>
|
||||||
|
<Dropdown.Toggle variant={variant}>{toggle}</Dropdown.Toggle>
|
||||||
|
|
||||||
|
<Dropdown.Menu>
|
||||||
|
{(stashBoxes?.length ?? 0) + scrapers.length > minFilteredScrapers && (
|
||||||
|
<ClearableInput
|
||||||
|
placeholder={`${intl.formatMessage({ id: "filter" })}...`}
|
||||||
|
value={filter}
|
||||||
|
setValue={setFilter}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
{filteredStashboxes.map((s, index) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={s.endpoint}
|
||||||
|
onClick={() =>
|
||||||
|
onScraperClicked({
|
||||||
|
stash_box_endpoint: s.endpoint,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{stashboxDisplayName(s.name, index)}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
|
||||||
|
{filteredStashboxes.length > 0 && filteredScrapers.length > 0 && (
|
||||||
|
<Dropdown.Divider />
|
||||||
|
)}
|
||||||
|
|
||||||
|
{filteredScrapers.map((s) => (
|
||||||
|
<Dropdown.Item
|
||||||
|
key={s.name}
|
||||||
|
onClick={() => onScraperClicked({ scraper_id: s.id })}
|
||||||
|
>
|
||||||
|
{s.name}
|
||||||
|
</Dropdown.Item>
|
||||||
|
))}
|
||||||
|
<Dropdown.Item onClick={() => onReloadScrapers()}>
|
||||||
|
<span className="fa-icon">
|
||||||
|
<Icon icon={faSyncAlt} />
|
||||||
|
</span>
|
||||||
|
<span>
|
||||||
|
<FormattedMessage id="actions.reload_scrapers" />
|
||||||
|
</span>
|
||||||
|
</Dropdown.Item>
|
||||||
|
</Dropdown.Menu>
|
||||||
|
</Dropdown>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -596,3 +596,12 @@ button.btn.favorite-button {
|
|||||||
.external-links-button {
|
.external-links-button {
|
||||||
display: inline-block;
|
display: inline-block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.scraper-menu .dropdown-menu {
|
||||||
|
min-width: 250px;
|
||||||
|
|
||||||
|
.dropdown-divider {
|
||||||
|
border-top-color: $textfield-bg;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user