diff --git a/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx b/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx
index 4b38100b5..6859fce18 100644
--- a/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx
+++ b/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx
@@ -1,4 +1,4 @@
-import React, { useState } from "react";
+import React, { PropsWithChildren, useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { Button } from "react-bootstrap";
import {
@@ -24,55 +24,154 @@ import {
InstalledScraperPackages,
} from "./ScraperPackageManager";
import { ExternalLink } from "../Shared/ExternalLink";
+import { ClearableInput } from "../Shared/ClearableInput";
+import { Counter } from "../Shared/Counter";
+
+const ScraperTable: React.FC<
+ PropsWithChildren<{
+ entityType: string;
+ count?: number;
+ }>
+> = ({ entityType, count, children }) => {
+ const intl = useIntl();
+
+ const titleEl = useMemo(() => {
+ const title = intl.formatMessage(
+ { id: "config.scraping.entity_scrapers" },
+ { entityType: intl.formatMessage({ id: entityType }) }
+ );
+
+ if (count) {
+ return (
+
+ {title}
+
+ );
+ }
+
+ return title;
+ }, [count, entityType, intl]);
+
+ return (
+
+
+
+
+
+
+ |
+
+
+ |
+
+
+ |
+
+
+ {children}
+
+
+ );
+};
+
+const ScrapeTypeList: React.FC<{
+ types: ScrapeType[];
+ entityType: string;
+}> = ({ types, entityType }) => {
+ const intl = useIntl();
+
+ const typeStrings = useMemo(
+ () =>
+ types.map((t) => {
+ switch (t) {
+ case ScrapeType.Fragment:
+ return intl.formatMessage(
+ { id: "config.scraping.entity_metadata" },
+ { entityType: intl.formatMessage({ id: entityType }) }
+ );
+ default:
+ return t;
+ }
+ }),
+ [types, entityType, intl]
+ );
+
+ return (
+
+ {typeStrings.map((t) => (
+ - {t}
+ ))}
+
+ );
+};
interface IURLList {
urls: string[];
}
const URLList: React.FC = ({ urls }) => {
- const maxCollapsedItems = 5;
- const [expanded, setExpanded] = useState(false);
-
- function linkSite(url: string) {
- const u = new URL(url);
- return `${u.protocol}//${u.host}`;
- }
-
- function renderLink(url?: string) {
- if (url) {
- const sanitised = TextUtils.sanitiseURL(url);
- const siteURL = linkSite(sanitised!);
-
- return {sanitised};
- }
- }
-
- function getListItems() {
- const items = urls.map((u) => {renderLink(u)});
-
- if (items.length > maxCollapsedItems) {
- if (!expanded) {
- items.length = maxCollapsedItems;
- }
-
- items.push(
-
-
-
- );
+ const items = useMemo(() => {
+ function linkSite(url: string) {
+ const u = new URL(url);
+ return `${u.protocol}//${u.host}`;
}
- return items;
- }
+ const ret = urls
+ .slice()
+ .sort()
+ .map((u) => {
+ const sanitised = TextUtils.sanitiseURL(u);
+ const siteURL = linkSite(sanitised!);
- return ;
+ return (
+
+ {sanitised}
+
+ );
+ });
+
+ return ret;
+ }, [urls]);
+
+ return ;
};
-export const SettingsScrapingPanel: React.FC = () => {
+const ScraperTableRow: React.FC<{
+ name: string;
+ entityType: string;
+ supportedScrapes: ScrapeType[];
+ urls: string[];
+}> = ({ name, entityType, supportedScrapes, urls }) => {
+ return (
+
+ {name} |
+
+
+ |
+
+
+ |
+
+ );
+};
+
+function filterScraper(filter: string) {
+ return (name: string, urls: string[] | undefined | null) => {
+ if (!filter) return true;
+
+ return (
+ name.toLowerCase().includes(filter) ||
+ urls?.some((url) => url.toLowerCase().includes(filter))
+ );
+ };
+}
+
+const ScrapersSection: React.FC = () => {
const Toast = useToast();
const intl = useIntl();
+
+ const [filter, setFilter] = useState("");
+
const { data: performerScrapers, loading: loadingPerformers } =
useListPerformerScrapers();
const { data: sceneScrapers, loading: loadingScenes } =
@@ -82,8 +181,29 @@ export const SettingsScrapingPanel: React.FC = () => {
const { data: groupScrapers, loading: loadingGroups } =
useListGroupScrapers();
- const { general, scraping, loading, error, saveGeneral, saveScraping } =
- useSettings();
+ const filteredScrapers = useMemo(() => {
+ const filterFn = filterScraper(filter.toLowerCase());
+ return {
+ performers: performerScrapers?.listScrapers.filter((s) =>
+ filterFn(s.name, s.performer?.urls)
+ ),
+ scenes: sceneScrapers?.listScrapers.filter((s) =>
+ filterFn(s.name, s.scene?.urls)
+ ),
+ galleries: galleryScrapers?.listScrapers.filter((s) =>
+ filterFn(s.name, s.gallery?.urls)
+ ),
+ groups: groupScrapers?.listScrapers.filter((s) =>
+ filterFn(s.name, s.group?.urls)
+ ),
+ };
+ }, [
+ performerScrapers,
+ sceneScrapers,
+ galleryScrapers,
+ groupScrapers,
+ filter,
+ ]);
async function onReloadScrapers() {
try {
@@ -93,213 +213,111 @@ export const SettingsScrapingPanel: React.FC = () => {
}
}
- function renderPerformerScrapeTypes(types: ScrapeType[]) {
- const typeStrings = types
- .filter((t) => t !== ScrapeType.Fragment)
- .map((t) => {
- switch (t) {
- case ScrapeType.Name:
- return intl.formatMessage({ id: "config.scraping.search_by_name" });
- default:
- return t;
- }
- });
-
+ if (loadingScenes || loadingGalleries || loadingPerformers || loadingGroups)
return (
-
- {typeStrings.map((t) => (
- - {t}
- ))}
-
+
+
+
);
- }
- function renderSceneScrapeTypes(types: ScrapeType[]) {
- const typeStrings = types.map((t) => {
- switch (t) {
- case ScrapeType.Fragment:
- return intl.formatMessage(
- { id: "config.scraping.entity_metadata" },
- { entityType: intl.formatMessage({ id: "scene" }) }
- );
- default:
- return t;
- }
- });
+ return (
+
+
+
setFilter(v)}
+ />
- return (
-
- {typeStrings.map((t) => (
- - {t}
- ))}
-
- );
- }
+
+
- function renderGalleryScrapeTypes(types: ScrapeType[]) {
- const typeStrings = types.map((t) => {
- switch (t) {
- case ScrapeType.Fragment:
- return intl.formatMessage(
- { id: "config.scraping.entity_metadata" },
- { entityType: intl.formatMessage({ id: "gallery" }) }
- );
- default:
- return t;
- }
- });
+
+ {!!filteredScrapers.scenes?.length && (
+
+ {filteredScrapers.scenes?.map((scraper) => (
+
+ ))}
+
+ )}
- return (
-
- {typeStrings.map((t) => (
- - {t}
- ))}
-
- );
- }
+ {!!filteredScrapers.galleries?.length && (
+
+ {filteredScrapers.galleries?.map((scraper) => (
+
+ ))}
+
+ )}
- function renderGroupScrapeTypes(types: ScrapeType[]) {
- const typeStrings = types.map((t) => {
- switch (t) {
- case ScrapeType.Fragment:
- return intl.formatMessage(
- { id: "config.scraping.entity_metadata" },
- { entityType: intl.formatMessage({ id: "group" }) }
- );
- default:
- return t;
- }
- });
+ {!!filteredScrapers.performers?.length && (
+
+ {filteredScrapers.performers?.map((scraper) => (
+
+ ))}
+
+ )}
- return (
-
- {typeStrings.map((t) => (
- - {t}
- ))}
-
- );
- }
+ {!!filteredScrapers.groups?.length && (
+
+ {filteredScrapers.groups?.map((scraper) => (
+
+ ))}
+
+ )}
+
+
+ );
+};
- function renderURLs(urls: string[]) {
- return ;
- }
-
- function renderSceneScrapers() {
- const elements = (sceneScrapers?.listScrapers ?? []).map((scraper) => (
-
- {scraper.name} |
-
- {renderSceneScrapeTypes(scraper.scene?.supported_scrapes ?? [])}
- |
- {renderURLs(scraper.scene?.urls ?? [])} |
-
- ));
-
- return renderTable(
- intl.formatMessage(
- { id: "config.scraping.entity_scrapers" },
- { entityType: intl.formatMessage({ id: "scene" }) }
- ),
- elements
- );
- }
-
- function renderGalleryScrapers() {
- const elements = (galleryScrapers?.listScrapers ?? []).map((scraper) => (
-
- {scraper.name} |
-
- {renderGalleryScrapeTypes(scraper.gallery?.supported_scrapes ?? [])}
- |
- {renderURLs(scraper.gallery?.urls ?? [])} |
-
- ));
-
- return renderTable(
- intl.formatMessage(
- { id: "config.scraping.entity_scrapers" },
- { entityType: intl.formatMessage({ id: "gallery" }) }
- ),
- elements
- );
- }
-
- function renderPerformerScrapers() {
- const elements = (performerScrapers?.listScrapers ?? []).map((scraper) => (
-
- {scraper.name} |
-
- {renderPerformerScrapeTypes(
- scraper.performer?.supported_scrapes ?? []
- )}
- |
- {renderURLs(scraper.performer?.urls ?? [])} |
-
- ));
-
- return renderTable(
- intl.formatMessage(
- { id: "config.scraping.entity_scrapers" },
- { entityType: intl.formatMessage({ id: "performer" }) }
- ),
- elements
- );
- }
-
- function renderGroupScrapers() {
- const elements = (groupScrapers?.listScrapers ?? []).map((scraper) => (
-
- {scraper.name} |
-
- {renderGroupScrapeTypes(scraper.group?.supported_scrapes ?? [])}
- |
- {renderURLs(scraper.group?.urls ?? [])} |
-
- ));
-
- return renderTable(
- intl.formatMessage(
- { id: "config.scraping.entity_scrapers" },
- { entityType: intl.formatMessage({ id: "group" }) }
- ),
- elements
- );
- }
-
- function renderTable(title: string, elements: JSX.Element[]) {
- if (elements.length > 0) {
- return (
-
-
-
-
- {intl.formatMessage({ id: "name" })} |
-
- {intl.formatMessage({
- id: "config.scraping.supported_types",
- })}
- |
-
- {intl.formatMessage({ id: "config.scraping.supported_urls" })}
- |
-
-
- {elements}
-
-
- );
- }
- }
+export const SettingsScrapingPanel: React.FC = () => {
+ const { general, scraping, loading, error, saveGeneral, saveScraping } =
+ useSettings();
if (error) return {error.message}
;
- if (
- loading ||
- loadingScenes ||
- loadingGalleries ||
- loadingPerformers ||
- loadingGroups
- )
- return ;
+ if (loading) return ;
return (
<>
@@ -345,25 +363,7 @@ export const SettingsScrapingPanel: React.FC = () => {
-
-
-
-
-
-
- {renderSceneScrapers()}
- {renderGalleryScrapers()}
- {renderPerformerScrapers()}
- {renderGroupScrapers()}
-
-
+
>
);
};
diff --git a/ui/v2.5/src/components/Settings/styles.scss b/ui/v2.5/src/components/Settings/styles.scss
index b7899d8d1..8861a8122 100644
--- a/ui/v2.5/src/components/Settings/styles.scss
+++ b/ui/v2.5/src/components/Settings/styles.scss
@@ -228,6 +228,7 @@
.scraper-table {
display: block;
margin-bottom: 16px;
+ max-height: 300px;
overflow: auto;
width: 100%;
@@ -247,6 +248,8 @@
ul {
margin-bottom: 0;
+ max-height: 100px;
+ overflow: auto;
padding-left: 0;
}
@@ -255,6 +258,11 @@
}
}
+.scraper-toolbar {
+ display: flex;
+ justify-content: space-between;
+}
+
.job-table.card {
background-color: $card-bg;
height: 10em;
diff --git a/ui/v2.5/src/components/Shared/CollapseButton.tsx b/ui/v2.5/src/components/Shared/CollapseButton.tsx
index 78099d0e8..7f70cf0ed 100644
--- a/ui/v2.5/src/components/Shared/CollapseButton.tsx
+++ b/ui/v2.5/src/components/Shared/CollapseButton.tsx
@@ -7,7 +7,7 @@ import { Button, Collapse } from "react-bootstrap";
import { Icon } from "./Icon";
interface IProps {
- text: string;
+ text: React.ReactNode;
}
export const CollapseButton: React.FC> = (