From 945d679158552a73eba5433dd7d500624c11c3a9 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 9 Dec 2025 07:29:41 +1100 Subject: [PATCH] Refactor and restyle scrape dialog on smaller viewports (#6387) * Improve string-list-input styling * Rename ScrapedDialog file * Move ScrapeDialog into separate file * Refactor scrape dialog row inputs * Refactor new value handling * Add context for labels * Refactor scrape dialog to accept children * Add existing/scraped labels for smaller viewports --- .../GalleryDetails/GalleryScrapeDialog.tsx | 9 +- .../Groups/GroupDetails/GroupScrapeDialog.tsx | 9 +- .../Images/ImageDetails/ImageScrapeDialog.tsx | 9 +- .../PerformerScrapeDialog.tsx | 29 +- .../Scenes/SceneDetails/SceneScrapeDialog.tsx | 9 +- .../components/Scenes/SceneMergeDialog.tsx | 65 +- .../Shared/ScrapeDialog/ScrapeDialog.tsx | 567 ++---------------- .../Shared/ScrapeDialog/ScrapeDialogRow.tsx | 433 +++++++++++++ .../Shared/ScrapeDialog/ScrapedObjectsRow.tsx | 102 +++- ui/v2.5/src/components/Shared/styles.scss | 23 +- ui/v2.5/src/locales/en-GB.json | 1 + 11 files changed, 658 insertions(+), 598 deletions(-) create mode 100644 ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialogRow.tsx diff --git a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx index fbfde9f97..6bc67b9bc 100644 --- a/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx +++ b/ui/v2.5/src/components/Galleries/GalleryDetails/GalleryScrapeDialog.tsx @@ -2,11 +2,11 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { - ScrapeDialog, ScrapedInputGroupRow, ScrapedStringListRow, ScrapedTextAreaRow, -} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; import { ObjectListScrapeResult, ObjectScrapeResult, @@ -225,10 +225,11 @@ export const GalleryScrapeDialog: React.FC = ({ { id: "dialogs.scrape_entity_title" }, { entity_type: intl.formatMessage({ id: "gallery" }) } )} - renderScrapeRows={renderScrapeRows} onClose={(apply) => { onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx b/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx index d37210c43..c9cbaf74e 100644 --- a/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx +++ b/ui/v2.5/src/components/Groups/GroupDetails/GroupScrapeDialog.tsx @@ -2,12 +2,12 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { - ScrapeDialog, ScrapedInputGroupRow, ScrapedImageRow, ScrapedTextAreaRow, ScrapedStringListRow, -} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; import TextUtils from "src/utils/text"; import { ObjectScrapeResult, @@ -224,10 +224,11 @@ export const GroupScrapeDialog: React.FC = ({ { id: "dialogs.scrape_entity_title" }, { entity_type: intl.formatMessage({ id: "group" }) } )} - renderScrapeRows={renderScrapeRows} onClose={(apply) => { onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx b/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx index 44b112078..ed95cb674 100644 --- a/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx +++ b/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx @@ -2,11 +2,11 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { - ScrapeDialog, ScrapedInputGroupRow, ScrapedStringListRow, ScrapedTextAreaRow, -} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; import { ObjectListScrapeResult, ObjectScrapeResult, @@ -226,10 +226,11 @@ export const ImageScrapeDialog: React.FC = ({ { id: "dialogs.scrape_entity_title" }, { entity_type: intl.formatMessage({ id: "image" }) } )} - renderScrapeRows={renderScrapeRows} onClose={(apply) => { onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx index ad7e44d6d..98c858037 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerScrapeDialog.tsx @@ -2,14 +2,14 @@ import React, { useState } from "react"; import { useIntl } from "react-intl"; import * as GQL from "src/core/generated-graphql"; import { - ScrapeDialog, ScrapedInputGroupRow, ScrapedImagesRow, ScrapeDialogRow, ScrapedTextAreaRow, ScrapedCountryRow, ScrapedStringListRow, -} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; import { Form } from "react-bootstrap"; import { genderStrings, @@ -66,12 +66,10 @@ function renderScrapedGenderRow( field="gender" title={title} result={result} - renderOriginalField={() => renderScrapedGender(result)} - renderNewField={() => - renderScrapedGender(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderScrapedGender(result)} + newField={renderScrapedGender(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} /> ); @@ -116,12 +114,10 @@ function renderScrapedCircumcisedRow( title={title} field="circumcised" result={result} - renderOriginalField={() => renderScrapedCircumcised(result)} - renderNewField={() => - renderScrapedCircumcised(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderScrapedCircumcised(result)} + newField={renderScrapedCircumcised(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} /> ); @@ -552,10 +548,11 @@ export const PerformerScrapeDialog: React.FC = ( { id: "dialogs.scrape_entity_title" }, { entity_type: intl.formatMessage({ id: "performer" }) } )} - renderScrapeRows={renderScrapeRows} onClose={(apply) => { props.onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index 7be291bd2..98939786d 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -1,12 +1,12 @@ import React, { useState } from "react"; import * as GQL from "src/core/generated-graphql"; import { - ScrapeDialog, ScrapedInputGroupRow, ScrapedTextAreaRow, ScrapedImageRow, ScrapedStringListRow, -} from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +} from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; import { useIntl } from "react-intl"; import { uniq } from "lodash-es"; import { Performer } from "src/components/Performers/PerformerSelect"; @@ -304,11 +304,12 @@ export const SceneScrapeDialog: React.FC = ({ { id: "dialogs.scrape_entity_title" }, { entity_type: intl.formatMessage({ id: "scene" }) } )} - renderScrapeRows={renderScrapeRows} onClose={(apply) => { onClose(apply ? makeNewScrapedItem() : undefined); }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx index 511ca2351..983978a27 100644 --- a/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneMergeDialog.tsx @@ -12,13 +12,13 @@ import { FormattedMessage, useIntl } from "react-intl"; import { useToast } from "src/hooks/Toast"; import { faExchangeAlt, faSignInAlt } from "@fortawesome/free-solid-svg-icons"; import { - ScrapeDialog, ScrapeDialogRow, ScrapedImageRow, ScrapedInputGroupRow, ScrapedStringListRow, ScrapedTextAreaRow, -} from "../Shared/ScrapeDialog/ScrapeDialog"; +} from "../Shared/ScrapeDialog/ScrapeDialogRow"; +import { ScrapeDialog } from "../Shared/ScrapeDialog/ScrapeDialog"; import { clone, uniq } from "lodash-es"; import { RatingSystem } from "src/components/Shared/Rating/RatingSystem"; import { ModalComponent } from "../Shared/Modal"; @@ -400,63 +400,59 @@ const SceneMergeDetails: React.FC = ({ field="rating" title={intl.formatMessage({ id: "rating" })} result={rating} - renderOriginalField={() => ( - - )} - renderNewField={() => ( - - )} + originalField={} + newField={} onChange={(value) => setRating(value)} /> ( + originalField={ {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setOCounter(value)} /> ( + originalField={ {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setPlayCount(value)} /> ( + originalField={ = ({ onChange={() => {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setPlayDuration(value)} /> ( + originalField={ = ({ isMulti isDisabled /> - )} - renderNewField={() => ( + } + newField={ = ({ isMulti isDisabled /> - )} + } onChange={(value) => setGalleries(value)} /> = ({ field="organized" title={intl.formatMessage({ id: "organized" })} result={organized} - renderOriginalField={() => ( + originalField={ {}} className="bg-secondary text-white border-secondary" /> - )} - renderNewField={() => ( + } + newField={ {}} className="bg-secondary text-white border-secondary" /> - )} + } onChange={(value) => setOrganized(value)} /> ( + originalField={ - )} - renderNewField={() => ( - - )} + } + newField={} onChange={(value) => setStashIDs(value)} /> = ({ title={dialogTitle} existingLabel={destinationLabel} scrapedLabel={sourceLabel} - renderScrapeRows={renderScrapeRows} onClose={(apply) => { if (!apply) { onClose(); @@ -642,7 +635,9 @@ const SceneMergeDetails: React.FC = ({ onClose(createValues()); } }} - /> + > + {renderScrapeRows()} + ); }; diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx index b67c55f41..98699cbb6 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialog.tsx @@ -1,457 +1,55 @@ -import React, { useState } from "react"; -import { - Form, - Col, - Row, - InputGroup, - Button, - FormControl, - Badge, -} from "react-bootstrap"; -import { CollapseButton } from "../CollapseButton"; -import { Icon } from "../Icon"; +import React, { useMemo } from "react"; +import { Form, Col, Row } from "react-bootstrap"; import { ModalComponent } from "../Modal"; -import clone from "lodash-es/clone"; import { FormattedMessage, useIntl } from "react-intl"; -import { - faCheck, - faPencilAlt, - faPlus, - faTimes, -} from "@fortawesome/free-solid-svg-icons"; -import { getCountryByISO } from "src/utils/country"; -import { CountrySelect } from "../CountrySelect"; -import { StringListInput } from "../StringListInput"; -import { ImageSelector } from "../ImageSelector"; -import { ScrapeResult } from "./scrapeResult"; +import { faPencilAlt } from "@fortawesome/free-solid-svg-icons"; import { useConfigurationContext } from "src/hooks/Config"; -interface IScrapedFieldProps { - result: ScrapeResult; +export interface IScrapeDialogContextState { + existingLabel?: React.ReactNode; + scrapedLabel?: React.ReactNode; } -interface IScrapedRowProps extends IScrapedFieldProps { - className?: string; - field: string; - title: string; - renderOriginalField: (result: ScrapeResult) => JSX.Element | undefined; - renderNewField: (result: ScrapeResult) => JSX.Element | undefined; - onChange: (value: ScrapeResult) => void; - newValues?: V[]; - onCreateNew?: (index: number) => void; - getName?: (value: V) => string; -} - -function renderButtonIcon(selected: boolean) { - const className = selected ? "text-success" : "text-muted"; - - return ( - - ); -} - -export const ScrapeDialogRow = (props: IScrapedRowProps) => { - const { getName = () => "" } = props; - - function handleSelectClick(isNew: boolean) { - const ret = clone(props.result); - ret.useNewValue = isNew; - props.onChange(ret); - } - - function hasNewValues() { - return props.newValues && props.newValues.length > 0 && props.onCreateNew; - } - - if (!props.result.scraped && !hasNewValues()) { - return <>; - } - - function renderNewValues() { - if (!hasNewValues()) { - return; - } - - const ret = ( - <> - {props.newValues!.map((t, i) => ( - props.onCreateNew!(i)} - > - {getName(t)} - - - ))} - - ); - - const minCollapseLength = 10; - - if (props.newValues!.length >= minCollapseLength) { - return ( - - {ret} - - ); - } - - return ret; - } - - return ( - - - {props.title} - - - - - - - - - - {props.renderOriginalField(props.result)} - - - - - - - - {props.renderNewField(props.result)} - - {renderNewValues()} - - - - - ); -}; - -interface IScrapedInputGroupProps { - isNew?: boolean; - placeholder?: string; - locked?: boolean; - result: ScrapeResult; - onChange?: (value: string) => void; -} - -const ScrapedInputGroup: React.FC = (props) => { - return ( - { - if (props.isNew && props.onChange) { - props.onChange(e.target.value); - } - }} - className="bg-secondary text-white border-secondary" - /> - ); -}; - -function getNameString(value: string) { - return value; -} - -interface IScrapedInputGroupRowProps { - title: string; - field: string; - className?: string; - placeholder?: string; - result: ScrapeResult; - locked?: boolean; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedInputGroupRow: React.FC = ( - props -) => { - return ( - ( - - )} - renderNewField={() => ( - - props.onChange(props.result.cloneWithValue(value)) - } - /> - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -interface IScrapedStringListProps { - isNew?: boolean; - placeholder?: string; - locked?: boolean; - result: ScrapeResult; - onChange?: (value: string[]) => void; -} - -const ScrapedStringList: React.FC = (props) => { - const value = props.isNew - ? props.result.newValue - : props.result.originalValue; - - return ( - { - if (props.isNew && props.onChange) { - props.onChange(v); - } - }} - placeholder={props.placeholder} - readOnly={!props.isNew || props.locked} - /> - ); -}; - -interface IScrapedStringListRowProps { - title: string; - field: string; - placeholder?: string; - result: ScrapeResult; - locked?: boolean; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedStringListRow: React.FC = ( - props -) => { - return ( - ( - - )} - renderNewField={() => ( - - props.onChange(props.result.cloneWithValue(value)) - } - /> - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -const ScrapedTextArea: React.FC = (props) => { - return ( - { - if (props.isNew && props.onChange) { - props.onChange(e.target.value); - } - }} - className="bg-secondary text-white border-secondary scene-description" - /> - ); -}; - -export const ScrapedTextAreaRow: React.FC = ( - props -) => { - return ( - ( - - )} - renderNewField={() => ( - - props.onChange(props.result.cloneWithValue(value)) - } - /> - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -interface IScrapedImageProps { - isNew?: boolean; - className?: string; - placeholder?: string; - result: ScrapeResult; -} - -const ScrapedImage: React.FC = (props) => { - const value = props.isNew - ? props.result.newValue - : props.result.originalValue; - - if (!value) { - return <>; - } - - return ( - {props.placeholder} - ); -}; - -interface IScrapedImageRowProps { - title: string; - field: string; - className?: string; - result: ScrapeResult; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedImageRow: React.FC = (props) => { - return ( - ( - - )} - renderNewField={() => ( - - )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; - -interface IScrapedImagesRowProps { - title: string; - field: string; - className?: string; - result: ScrapeResult; - images: string[]; - onChange: (value: ScrapeResult) => void; -} - -export const ScrapedImagesRow: React.FC = (props) => { - const [imageIndex, setImageIndex] = useState(0); - - function onSetImageIndex(newIdx: number) { - const ret = props.result.cloneWithValue(props.images[newIdx]); - props.onChange(ret); - setImageIndex(newIdx); - } - - return ( - ( - - )} - renderNewField={() => ( -
- -
- )} - onChange={props.onChange} - getName={getNameString} - /> - ); -}; +export const ScrapeDialogContext = + React.createContext({}); interface IScrapeDialogProps { title: string; - existingLabel?: string; - scrapedLabel?: string; - renderScrapeRows: () => JSX.Element; + existingLabel?: React.ReactNode; + scrapedLabel?: React.ReactNode; onClose: (apply?: boolean) => void; } -export const ScrapeDialog: React.FC = ( - props: IScrapeDialogProps -) => { +export const ScrapeDialog: React.FC< + React.PropsWithChildren +> = (props: React.PropsWithChildren) => { const intl = useIntl(); const { configuration } = useConfigurationContext(); const { sfwContentMode } = configuration.interface; + const existingLabel = useMemo( + () => + props.existingLabel ?? ( + + ), + [props.existingLabel] + ); + const scrapedLabel = useMemo( + () => + props.scrapedLabel ?? ( + + ), + [props.scrapedLabel] + ); + + const contextState = useMemo( + () => ({ + existingLabel: existingLabel, + scrapedLabel: scrapedLabel, + }), + [existingLabel, scrapedLabel] + ); + return ( = ( }} >
-
- - - - - {props.existingLabel ?? ( - - )} - - - {props.scrapedLabel ?? ( - - )} - - - - + + + + + + + {existingLabel} + + + {scrapedLabel} + + + + - {props.renderScrapeRows()} - + {props.children} + +
); }; - -interface IScrapedCountryRowProps { - title: string; - field: string; - result: ScrapeResult; - onChange: (value: ScrapeResult) => void; - locked?: boolean; - locale?: string; -} - -export const ScrapedCountryRow: React.FC = ({ - title, - field, - result, - onChange, - locked, - locale, -}) => ( - ( - - )} - renderNewField={() => ( - { - if (onChange) { - onChange(result.cloneWithValue(value)); - } - }} - showFlag={false} - isClearable={false} - className="flex-grow-1" - /> - )} - onChange={onChange} - getName={getNameString} - /> -); diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialogRow.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialogRow.tsx new file mode 100644 index 000000000..88b79d87d --- /dev/null +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapeDialogRow.tsx @@ -0,0 +1,433 @@ +import React, { useContext, useState } from "react"; +import { + Form, + Col, + Row, + InputGroup, + Button, + FormControl, +} from "react-bootstrap"; +import { Icon } from "../Icon"; +import clone from "lodash-es/clone"; +import { faCheck, faTimes } from "@fortawesome/free-solid-svg-icons"; +import { getCountryByISO } from "src/utils/country"; +import { CountrySelect } from "../CountrySelect"; +import { StringListInput } from "../StringListInput"; +import { ImageSelector } from "../ImageSelector"; +import { ScrapeResult } from "./scrapeResult"; +import { ScrapeDialogContext } from "./ScrapeDialog"; + +function renderButtonIcon(selected: boolean) { + const className = selected ? "text-success" : "text-muted"; + + return ( + + ); +} + +interface IScrapedFieldProps { + result: ScrapeResult; +} + +interface IScrapedRowProps extends IScrapedFieldProps { + className?: string; + field: string; + title: string; + originalField: React.ReactNode; + newField: React.ReactNode; + onChange: (value: ScrapeResult) => void; + newValues?: React.ReactNode; +} + +export const ScrapeDialogRow = (props: IScrapedRowProps) => { + const { existingLabel, scrapedLabel } = useContext(ScrapeDialogContext); + + function handleSelectClick(isNew: boolean) { + const ret = clone(props.result); + ret.useNewValue = isNew; + props.onChange(ret); + } + + if (!props.result.scraped && !props.newValues) { + return <>; + } + + return ( + + + {props.title} + + + + + + {existingLabel} + + + + + + + {props.originalField} + + + + + {scrapedLabel} + + + + + + + {props.newField} + + {props.newValues} + + + + + ); +}; + +interface IScrapedInputGroupProps { + isNew?: boolean; + placeholder?: string; + locked?: boolean; + result: ScrapeResult; + onChange?: (value: string) => void; +} + +const ScrapedInputGroup: React.FC = (props) => { + return ( + { + if (props.isNew && props.onChange) { + props.onChange(e.target.value); + } + }} + className="bg-secondary text-white border-secondary" + /> + ); +}; + +interface IScrapedInputGroupRowProps { + title: string; + field: string; + className?: string; + placeholder?: string; + result: ScrapeResult; + locked?: boolean; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedInputGroupRow: React.FC = ( + props +) => { + return ( + + } + newField={ + + props.onChange(props.result.cloneWithValue(value)) + } + /> + } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedStringListProps { + isNew?: boolean; + placeholder?: string; + locked?: boolean; + result: ScrapeResult; + onChange?: (value: string[]) => void; +} + +const ScrapedStringList: React.FC = (props) => { + const value = props.isNew + ? props.result.newValue + : props.result.originalValue; + + return ( + { + if (props.isNew && props.onChange) { + props.onChange(v); + } + }} + placeholder={props.placeholder} + readOnly={!props.isNew || props.locked} + /> + ); +}; + +interface IScrapedStringListRowProps { + title: string; + field: string; + placeholder?: string; + result: ScrapeResult; + locked?: boolean; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedStringListRow: React.FC = ( + props +) => { + return ( + + } + newField={ + + props.onChange(props.result.cloneWithValue(value)) + } + /> + } + onChange={props.onChange} + /> + ); +}; + +const ScrapedTextArea: React.FC = (props) => { + return ( + { + if (props.isNew && props.onChange) { + props.onChange(e.target.value); + } + }} + className="bg-secondary text-white border-secondary scene-description" + /> + ); +}; + +export const ScrapedTextAreaRow: React.FC = ( + props +) => { + return ( + + } + newField={ + + props.onChange(props.result.cloneWithValue(value)) + } + /> + } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedImageProps { + isNew?: boolean; + className?: string; + placeholder?: string; + result: ScrapeResult; +} + +const ScrapedImage: React.FC = (props) => { + const value = props.isNew + ? props.result.newValue + : props.result.originalValue; + + if (!value) { + return <>; + } + + return ( + {props.placeholder} + ); +}; + +interface IScrapedImageRowProps { + title: string; + field: string; + className?: string; + result: ScrapeResult; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedImageRow: React.FC = (props) => { + return ( + + } + newField={ + + } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedImagesRowProps { + title: string; + field: string; + className?: string; + result: ScrapeResult; + images: string[]; + onChange: (value: ScrapeResult) => void; +} + +export const ScrapedImagesRow: React.FC = (props) => { + const [imageIndex, setImageIndex] = useState(0); + + function onSetImageIndex(newIdx: number) { + const ret = props.result.cloneWithValue(props.images[newIdx]); + props.onChange(ret); + setImageIndex(newIdx); + } + + return ( + + } + newField={ +
+ +
+ } + onChange={props.onChange} + /> + ); +}; + +interface IScrapedCountryRowProps { + title: string; + field: string; + result: ScrapeResult; + onChange: (value: ScrapeResult) => void; + locked?: boolean; + locale?: string; +} + +export const ScrapedCountryRow: React.FC = ({ + title, + field, + result, + onChange, + locked, + locale, +}) => ( + + } + newField={ + { + if (onChange) { + onChange(result.cloneWithValue(value)); + } + }} + showFlag={false} + isClearable={false} + className="flex-grow-1" + /> + } + onChange={onChange} + /> +); diff --git a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx index 6aa985796..1588b8829 100644 --- a/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx +++ b/ui/v2.5/src/components/Shared/ScrapeDialog/ScrapedObjectsRow.tsx @@ -1,6 +1,6 @@ import React, { useMemo } from "react"; import * as GQL from "src/core/generated-graphql"; -import { ScrapeDialogRow } from "src/components/Shared/ScrapeDialog/ScrapeDialog"; +import { ScrapeDialogRow } from "src/components/Shared/ScrapeDialog/ScrapeDialogRow"; import { PerformerSelect } from "src/components/Performers/PerformerSelect"; import { ObjectScrapeResult, @@ -10,6 +10,58 @@ import { TagIDSelect } from "src/components/Tags/TagSelect"; import { StudioSelect } from "src/components/Studios/StudioSelect"; import { GroupSelect } from "src/components/Groups/GroupSelect"; import { uniq } from "lodash-es"; +import { CollapseButton } from "../CollapseButton"; +import { Badge, Button } from "react-bootstrap"; +import { Icon } from "../Icon"; +import { faPlus } from "@fortawesome/free-solid-svg-icons"; +import { useIntl } from "react-intl"; + +interface INewScrapedObjects { + newValues: T[]; + onCreateNew: (value: T) => void; + getName: (value: T) => string; +} + +export const NewScrapedObjects = (props: INewScrapedObjects) => { + const intl = useIntl(); + + if (props.newValues.length === 0) { + return null; + } + + const ret = ( + <> + {props.newValues.map((t) => ( + props.onCreateNew(t)} + > + {props.getName(t)} + + + ))} + + ); + + const minCollapseLength = 10; + + if (props.newValues!.length >= minCollapseLength) { + const missingText = intl.formatMessage({ + id: "dialogs.scrape_results_missing", + }); + return ( + + {ret} + + ); + } + + return ret; +}; interface IScrapedStudioRow { title: string; @@ -77,18 +129,20 @@ export const ScrapedStudioRow: React.FC = ({ title={title} field={field} result={result} - renderOriginalField={() => renderScrapedStudio(result)} - renderNewField={() => - renderScrapedStudio(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderScrapedStudio(result)} + newField={renderScrapedStudio(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} - newValues={newStudio ? [newStudio] : undefined} - onCreateNew={() => { - if (onCreateNew && newStudio) onCreateNew(newStudio); - }} - getName={getObjectName} + newValues={ + newStudio && onCreateNew ? ( + + ) : undefined + } /> ); }; @@ -125,18 +179,20 @@ export const ScrapedObjectsRow = (props: IScrapedObjectsRow) => { title={title} field={field} result={result} - renderOriginalField={() => renderObjects(result)} - renderNewField={() => - renderObjects(result, true, (value) => - onChange(result.cloneWithValue(value)) - ) - } + originalField={renderObjects(result)} + newField={renderObjects(result, true, (value) => + onChange(result.cloneWithValue(value)) + )} onChange={onChange} - newValues={newObjects} - onCreateNew={(i) => { - if (onCreateNew) onCreateNew(newObjects![i]); - }} - getName={getName} + newValues={ + onCreateNew ? ( + + ) : undefined + } /> ); }; diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 947dd22d7..ce8454b89 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -155,6 +155,15 @@ } .scrape-dialog { + .column-label { + color: $muted-gray; + font-size: 0.85em; + } + + .string-list-input { + width: 100%; + } + .modal-content .dialog-container { max-height: calc(100vh - 14rem); overflow-y: auto; @@ -391,8 +400,18 @@ button.collapse-button { opacity: 0.5; } -.string-list-input .input-group { - margin-bottom: 0.35rem; +.string-list-input { + .form-group { + margin-bottom: 0; + } + + .input-group { + margin-bottom: 0.35rem; + + &:last-child { + margin-bottom: 0; + } + } } .bulk-update-text-input { diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 54982b932..a78ca55ec 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -1020,6 +1020,7 @@ "scrape_entity_query": "{entity_type} Scrape Query", "scrape_entity_title": "{entity_type} Scrape Results", "scrape_results_existing": "Existing", + "scrape_results_missing": "Missing", "scrape_results_scraped": "Scraped", "set_default_filter_confirm": "Are you sure you want to set this filter as the default?", "set_image_url_title": "Image URL",