From b69d9cc8409c9dae371693dee311f5b98a7930b6 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Thu, 4 Jul 2024 09:09:31 +1000 Subject: [PATCH] Metadata Providers -> Scraper list improvements (#5040) * Refactor scraping settings panel * Add max-height to scraper table * Separate scraper section * Add filter to scrapers section * Add counters to scraper headings * Show all urls with a scrollbar * Sort URLs --- .../Settings/SettingsScrapingPanel.tsx | 512 +++++++++--------- ui/v2.5/src/components/Settings/styles.scss | 8 + .../src/components/Shared/CollapseButton.tsx | 2 +- 3 files changed, 265 insertions(+), 257 deletions(-) diff --git a/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx b/ui/v2.5/src/components/Settings/SettingsScrapingPanel.tsx index 4b38100b53a..6859fce1862 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(); -interface IURLList { - urls: string[]; -} + const titleEl = useMemo(() => { + const title = intl.formatMessage( + { id: "config.scraping.entity_scrapers" }, + { entityType: intl.formatMessage({ id: entityType }) } + ); -const URLList: React.FC = ({ urls }) => { - const maxCollapsedItems = 5; - const [expanded, setExpanded] = useState(false); + if (count) { + return ( + + {title} + + ); + } - function linkSite(url: string) { - const u = new URL(url); - return `${u.protocol}//${u.host}`; - } + return title; + }, [count, entityType, intl]); - function renderLink(url?: string) { - if (url) { - const sanitised = TextUtils.sanitiseURL(url); - const siteURL = linkSite(sanitised!); + return ( + + + + + + + + + + {children} +
+ + + + + +
+
+ ); +}; - return {sanitised}; - } - } +const ScrapeTypeList: React.FC<{ + types: ScrapeType[]; + entityType: string; +}> = ({ types, entityType }) => { + const intl = useIntl(); - function getListItems() { - const items = urls.map((u) =>
  • {renderLink(u)}
  • ); + 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] + ); - if (items.length > maxCollapsedItems) { - if (!expanded) { - items.length = maxCollapsedItems; - } + return ( + + ); +}; - items.push( -
  • - -
  • - ); +interface IURLList { + urls: string[]; +} + +const URLList: React.FC = ({ urls }) => { + 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 ( +
  • + {sanitised} +
  • + ); + }); + + return ret; + }, [urls]); - return
      {getListItems()}
    ; + return
      {items}
    ; }; -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; - } - }); - - 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 ( -
      - {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; - } - }); - - return ( -
      - {typeStrings.map((t) => ( -
    • {t}
    • - ))} -
    - ); - } - - 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; - } - }); - + if (loadingScenes || loadingGalleries || loadingPerformers || loadingGroups) return ( -
      - {typeStrings.map((t) => ( -
    • {t}
    • - ))} -
    - ); - } - - 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 - ); - } + return ( + +
    + setFilter(v)} + /> - 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 - ); - } + +
    + +
    + {!!filteredScrapers.scenes?.length && ( + + {filteredScrapers.scenes?.map((scraper) => ( + + ))} + + )} + + {!!filteredScrapers.galleries?.length && ( + + {filteredScrapers.galleries?.map((scraper) => ( + + ))} + + )} + + {!!filteredScrapers.performers?.length && ( + + {filteredScrapers.performers?.map((scraper) => ( + + ))} + + )} + + {!!filteredScrapers.groups?.length && ( + + {filteredScrapers.groups?.map((scraper) => ( + + ))} + + )} +
    +
    + ); +}; - function renderTable(title: string, elements: JSX.Element[]) { - if (elements.length > 0) { - return ( - - - - - - - - - - {elements} -
    {intl.formatMessage({ id: "name" })} - {intl.formatMessage({ - id: "config.scraping.supported_types", - })} - - {intl.formatMessage({ id: "config.scraping.supported_urls" })} -
    -
    - ); - } - } +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 b7899d8d13d..8861a8122cc 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 78099d0e85a..7f70cf0ed37 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> = (