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
This commit is contained in:
WithoutPants
2025-12-09 07:29:41 +11:00
committed by GitHub
parent 7db394bbea
commit 945d679158
11 changed files with 658 additions and 598 deletions

View File

@@ -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<IGalleryScrapeDialogProps> = ({
{ id: "dialogs.scrape_entity_title" },
{ entity_type: intl.formatMessage({ id: "gallery" }) }
)}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
onClose(apply ? makeNewScrapedItem() : undefined);
}}
/>
>
{renderScrapeRows()}
</ScrapeDialog>
);
};

View File

@@ -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<IGroupScrapeDialogProps> = ({
{ id: "dialogs.scrape_entity_title" },
{ entity_type: intl.formatMessage({ id: "group" }) }
)}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
onClose(apply ? makeNewScrapedItem() : undefined);
}}
/>
>
{renderScrapeRows()}
</ScrapeDialog>
);
};

View File

@@ -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<IImageScrapeDialogProps> = ({
{ id: "dialogs.scrape_entity_title" },
{ entity_type: intl.formatMessage({ id: "image" }) }
)}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
onClose(apply ? makeNewScrapedItem() : undefined);
}}
/>
>
{renderScrapeRows()}
</ScrapeDialog>
);
};

View File

@@ -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<IPerformerScrapeDialogProps> = (
{ id: "dialogs.scrape_entity_title" },
{ entity_type: intl.formatMessage({ id: "performer" }) }
)}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
props.onClose(apply ? makeNewScrapedItem() : undefined);
}}
/>
>
{renderScrapeRows()}
</ScrapeDialog>
);
};

View File

@@ -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<ISceneScrapeDialogProps> = ({
{ id: "dialogs.scrape_entity_title" },
{ entity_type: intl.formatMessage({ id: "scene" }) }
)}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
onClose(apply ? makeNewScrapedItem() : undefined);
}}
/>
>
{renderScrapeRows()}
</ScrapeDialog>
);
};

View File

@@ -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<ISceneMergeDetailsProps> = ({
field="rating"
title={intl.formatMessage({ id: "rating" })}
result={rating}
renderOriginalField={() => (
<RatingSystem value={rating.originalValue} disabled />
)}
renderNewField={() => (
<RatingSystem value={rating.newValue} disabled />
)}
originalField={<RatingSystem value={rating.originalValue} disabled />}
newField={<RatingSystem value={rating.newValue} disabled />}
onChange={(value) => setRating(value)}
/>
<ScrapeDialogRow
field="o_count"
title={intl.formatMessage({ id: "o_count" })}
result={oCounter}
renderOriginalField={() => (
originalField={
<FormControl
value={oCounter.originalValue ?? 0}
readOnly
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
renderNewField={() => (
}
newField={
<FormControl
value={oCounter.newValue ?? 0}
readOnly
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
}
onChange={(value) => setOCounter(value)}
/>
<ScrapeDialogRow
field="play_count"
title={intl.formatMessage({ id: "play_count" })}
result={playCount}
renderOriginalField={() => (
originalField={
<FormControl
value={playCount.originalValue ?? 0}
readOnly
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
renderNewField={() => (
}
newField={
<FormControl
value={playCount.newValue ?? 0}
readOnly
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
}
onChange={(value) => setPlayCount(value)}
/>
<ScrapeDialogRow
field="play_duration"
title={intl.formatMessage({ id: "play_duration" })}
result={playDuration}
renderOriginalField={() => (
originalField={
<FormControl
value={TextUtils.secondsToTimestamp(
playDuration.originalValue ?? 0
@@ -465,22 +461,22 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
renderNewField={() => (
}
newField={
<FormControl
value={TextUtils.secondsToTimestamp(playDuration.newValue ?? 0)}
readOnly
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
}
onChange={(value) => setPlayDuration(value)}
/>
<ScrapeDialogRow
field="galleries"
title={intl.formatMessage({ id: "galleries" })}
result={galleries}
renderOriginalField={() => (
originalField={
<GallerySelect
className="form-control react-select"
ids={galleries.originalValue ?? []}
@@ -488,8 +484,8 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
isMulti
isDisabled
/>
)}
renderNewField={() => (
}
newField={
<GallerySelect
className="form-control react-select"
ids={galleries.newValue ?? []}
@@ -497,7 +493,7 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
isMulti
isDisabled
/>
)}
}
onChange={(value) => setGalleries(value)}
/>
<ScrapedStudioRow
@@ -535,34 +531,32 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
field="organized"
title={intl.formatMessage({ id: "organized" })}
result={organized}
renderOriginalField={() => (
originalField={
<FormControl
value={organized.originalValue ? trueString : falseString}
readOnly
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
renderNewField={() => (
}
newField={
<FormControl
value={organized.newValue ? trueString : falseString}
readOnly
onChange={() => {}}
className="bg-secondary text-white border-secondary"
/>
)}
}
onChange={(value) => setOrganized(value)}
/>
<ScrapeDialogRow
field="stash_ids"
title={intl.formatMessage({ id: "stash_id" })}
result={stashIDs}
renderOriginalField={() => (
originalField={
<StashIDsField values={stashIDs?.originalValue ?? []} />
)}
renderNewField={() => (
<StashIDsField values={stashIDs?.newValue ?? []} />
)}
}
newField={<StashIDsField values={stashIDs?.newValue ?? []} />}
onChange={(value) => setStashIDs(value)}
/>
<ScrapedImageRow
@@ -634,7 +628,6 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
title={dialogTitle}
existingLabel={destinationLabel}
scrapedLabel={sourceLabel}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
if (!apply) {
onClose();
@@ -642,7 +635,9 @@ const SceneMergeDetails: React.FC<ISceneMergeDetailsProps> = ({
onClose(createValues());
}
}}
/>
>
{renderScrapeRows()}
</ScrapeDialog>
);
};

View File

@@ -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<T> {
result: ScrapeResult<T>;
export interface IScrapeDialogContextState {
existingLabel?: React.ReactNode;
scrapedLabel?: React.ReactNode;
}
interface IScrapedRowProps<T, V> extends IScrapedFieldProps<T> {
className?: string;
field: string;
title: string;
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
renderNewField: (result: ScrapeResult<T>) => JSX.Element | undefined;
onChange: (value: ScrapeResult<T>) => void;
newValues?: V[];
onCreateNew?: (index: number) => void;
getName?: (value: V) => string;
}
function renderButtonIcon(selected: boolean) {
const className = selected ? "text-success" : "text-muted";
return (
<Icon
className={`fa-fw ${className}`}
icon={selected ? faCheck : faTimes}
/>
);
}
export const ScrapeDialogRow = <T, V>(props: IScrapedRowProps<T, V>) => {
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) => (
<Badge
className="tag-item"
variant="secondary"
key={getName(t)}
onClick={() => props.onCreateNew!(i)}
>
{getName(t)}
<Button className="minimal ml-2">
<Icon className="fa-fw" icon={faPlus} />
</Button>
</Badge>
))}
</>
);
const minCollapseLength = 10;
if (props.newValues!.length >= minCollapseLength) {
return (
<CollapseButton text={`Missing (${props.newValues!.length})`}>
{ret}
</CollapseButton>
);
}
return ret;
}
return (
<Row
className={`px-3 pt-3 ${props.className ?? ""}`}
data-field={props.field}
>
<Form.Label column lg="3">
{props.title}
</Form.Label>
<Col lg="9">
<Row>
<Col xs="6">
<InputGroup>
<InputGroup.Prepend className="bg-secondary text-white border-secondary">
<Button
variant="secondary"
onClick={() => handleSelectClick(false)}
>
{renderButtonIcon(!props.result.useNewValue)}
</Button>
</InputGroup.Prepend>
{props.renderOriginalField(props.result)}
</InputGroup>
</Col>
<Col xs="6">
<InputGroup>
<InputGroup.Prepend>
<Button
variant="secondary"
onClick={() => handleSelectClick(true)}
>
{renderButtonIcon(props.result.useNewValue)}
</Button>
</InputGroup.Prepend>
{props.renderNewField(props.result)}
</InputGroup>
{renderNewValues()}
</Col>
</Row>
</Col>
</Row>
);
};
interface IScrapedInputGroupProps {
isNew?: boolean;
placeholder?: string;
locked?: boolean;
result: ScrapeResult<string>;
onChange?: (value: string) => void;
}
const ScrapedInputGroup: React.FC<IScrapedInputGroupProps> = (props) => {
return (
<FormControl
placeholder={props.placeholder}
value={props.isNew ? props.result.newValue : props.result.originalValue}
readOnly={!props.isNew || props.locked}
onChange={(e) => {
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<string>;
locked?: boolean;
onChange: (value: ScrapeResult<string>) => void;
}
export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
props
) => {
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
className={props.className}
result={props.result}
renderOriginalField={() => (
<ScrapedInputGroup
placeholder={props.placeholder || props.title}
result={props.result}
/>
)}
renderNewField={() => (
<ScrapedInputGroup
placeholder={props.placeholder || props.title}
result={props.result}
isNew
locked={props.locked}
onChange={(value) =>
props.onChange(props.result.cloneWithValue(value))
}
/>
)}
onChange={props.onChange}
getName={getNameString}
/>
);
};
interface IScrapedStringListProps {
isNew?: boolean;
placeholder?: string;
locked?: boolean;
result: ScrapeResult<string[]>;
onChange?: (value: string[]) => void;
}
const ScrapedStringList: React.FC<IScrapedStringListProps> = (props) => {
const value = props.isNew
? props.result.newValue
: props.result.originalValue;
return (
<StringListInput
value={value ?? []}
setValue={(v) => {
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<string[]>;
locked?: boolean;
onChange: (value: ScrapeResult<string[]>) => void;
}
export const ScrapedStringListRow: React.FC<IScrapedStringListRowProps> = (
props
) => {
return (
<ScrapeDialogRow
className="string-list-row"
title={props.title}
field={props.field}
result={props.result}
renderOriginalField={() => (
<ScrapedStringList
placeholder={props.placeholder || props.title}
result={props.result}
/>
)}
renderNewField={() => (
<ScrapedStringList
placeholder={props.placeholder || props.title}
result={props.result}
isNew
locked={props.locked}
onChange={(value) =>
props.onChange(props.result.cloneWithValue(value))
}
/>
)}
onChange={props.onChange}
getName={getNameString}
/>
);
};
const ScrapedTextArea: React.FC<IScrapedInputGroupProps> = (props) => {
return (
<FormControl
as="textarea"
placeholder={props.placeholder}
value={props.isNew ? props.result.newValue : props.result.originalValue}
readOnly={!props.isNew}
onChange={(e) => {
if (props.isNew && props.onChange) {
props.onChange(e.target.value);
}
}}
className="bg-secondary text-white border-secondary scene-description"
/>
);
};
export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
props
) => {
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
result={props.result}
renderOriginalField={() => (
<ScrapedTextArea
placeholder={props.placeholder || props.title}
result={props.result}
/>
)}
renderNewField={() => (
<ScrapedTextArea
placeholder={props.placeholder || props.title}
result={props.result}
isNew
onChange={(value) =>
props.onChange(props.result.cloneWithValue(value))
}
/>
)}
onChange={props.onChange}
getName={getNameString}
/>
);
};
interface IScrapedImageProps {
isNew?: boolean;
className?: string;
placeholder?: string;
result: ScrapeResult<string>;
}
const ScrapedImage: React.FC<IScrapedImageProps> = (props) => {
const value = props.isNew
? props.result.newValue
: props.result.originalValue;
if (!value) {
return <></>;
}
return (
<img className={props.className} src={value} alt={props.placeholder} />
);
};
interface IScrapedImageRowProps {
title: string;
field: string;
className?: string;
result: ScrapeResult<string>;
onChange: (value: ScrapeResult<string>) => void;
}
export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
result={props.result}
renderOriginalField={() => (
<ScrapedImage
result={props.result}
className={props.className}
placeholder={props.title}
/>
)}
renderNewField={() => (
<ScrapedImage
result={props.result}
className={props.className}
placeholder={props.title}
isNew
/>
)}
onChange={props.onChange}
getName={getNameString}
/>
);
};
interface IScrapedImagesRowProps {
title: string;
field: string;
className?: string;
result: ScrapeResult<string>;
images: string[];
onChange: (value: ScrapeResult<string>) => void;
}
export const ScrapedImagesRow: React.FC<IScrapedImagesRowProps> = (props) => {
const [imageIndex, setImageIndex] = useState(0);
function onSetImageIndex(newIdx: number) {
const ret = props.result.cloneWithValue(props.images[newIdx]);
props.onChange(ret);
setImageIndex(newIdx);
}
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
result={props.result}
renderOriginalField={() => (
<ScrapedImage
result={props.result}
className={props.className}
placeholder={props.title}
/>
)}
renderNewField={() => (
<div className="image-selection-parent">
<ImageSelector
imageClassName={props.className}
images={props.images}
imageIndex={imageIndex}
setImageIndex={onSetImageIndex}
/>
</div>
)}
onChange={props.onChange}
getName={getNameString}
/>
);
};
export const ScrapeDialogContext =
React.createContext<IScrapeDialogContextState>({});
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<IScrapeDialogProps> = (
props: IScrapeDialogProps
) => {
export const ScrapeDialog: React.FC<
React.PropsWithChildren<IScrapeDialogProps>
> = (props: React.PropsWithChildren<IScrapeDialogProps>) => {
const intl = useIntl();
const { configuration } = useConfigurationContext();
const { sfwContentMode } = configuration.interface;
const existingLabel = useMemo(
() =>
props.existingLabel ?? (
<FormattedMessage id="dialogs.scrape_results_existing" />
),
[props.existingLabel]
);
const scrapedLabel = useMemo(
() =>
props.scrapedLabel ?? (
<FormattedMessage id="dialogs.scrape_results_scraped" />
),
[props.scrapedLabel]
);
const contextState = useMemo(
() => ({
existingLabel: existingLabel,
scrapedLabel: scrapedLabel,
}),
[existingLabel, scrapedLabel]
);
return (
<ModalComponent
show
@@ -474,76 +72,33 @@ export const ScrapeDialog: React.FC<IScrapeDialogProps> = (
}}
>
<div className="dialog-container">
<Form>
<Row className="px-3 pt-3">
<Col lg={{ span: 9, offset: 3 }}>
<Row>
<Form.Label column xs="6">
{props.existingLabel ?? (
<FormattedMessage id="dialogs.scrape_results_existing" />
)}
</Form.Label>
<Form.Label column xs="6">
{props.scrapedLabel ?? (
<FormattedMessage id="dialogs.scrape_results_scraped" />
)}
</Form.Label>
</Row>
</Col>
</Row>
<ScrapeDialogContext.Provider value={contextState}>
<Form>
<Row className="px-3 pt-3">
<Col lg={{ span: 9, offset: 3 }}>
<Row>
<Form.Label
column
lg="6"
className="d-lg-block d-none column-label"
>
{existingLabel}
</Form.Label>
<Form.Label
column
lg="6"
className="d-lg-block d-none column-label"
>
{scrapedLabel}
</Form.Label>
</Row>
</Col>
</Row>
{props.renderScrapeRows()}
</Form>
{props.children}
</Form>
</ScrapeDialogContext.Provider>
</div>
</ModalComponent>
);
};
interface IScrapedCountryRowProps {
title: string;
field: string;
result: ScrapeResult<string>;
onChange: (value: ScrapeResult<string>) => void;
locked?: boolean;
locale?: string;
}
export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
title,
field,
result,
onChange,
locked,
locale,
}) => (
<ScrapeDialogRow
title={title}
field={field}
result={result}
renderOriginalField={() => (
<FormControl
value={
getCountryByISO(result.originalValue, locale) ?? result.originalValue
}
readOnly
className="bg-secondary text-white border-secondary"
/>
)}
renderNewField={() => (
<CountrySelect
value={result.newValue}
disabled={locked}
onChange={(value) => {
if (onChange) {
onChange(result.cloneWithValue(value));
}
}}
showFlag={false}
isClearable={false}
className="flex-grow-1"
/>
)}
onChange={onChange}
getName={getNameString}
/>
);

View File

@@ -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 (
<Icon
className={`fa-fw ${className}`}
icon={selected ? faCheck : faTimes}
/>
);
}
interface IScrapedFieldProps<T> {
result: ScrapeResult<T>;
}
interface IScrapedRowProps<T> extends IScrapedFieldProps<T> {
className?: string;
field: string;
title: string;
originalField: React.ReactNode;
newField: React.ReactNode;
onChange: (value: ScrapeResult<T>) => void;
newValues?: React.ReactNode;
}
export const ScrapeDialogRow = <T,>(props: IScrapedRowProps<T>) => {
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 (
<Row
className={`px-3 pt-3 ${props.className ?? ""}`}
data-field={props.field}
>
<Form.Label column lg="3">
{props.title}
</Form.Label>
<Col lg="9">
<Row>
<Form.Label column className="d-lg-none column-label">
{existingLabel}
</Form.Label>
<Col lg="6">
<InputGroup>
<InputGroup.Prepend className="bg-secondary text-white border-secondary">
<Button
variant="secondary"
onClick={() => handleSelectClick(false)}
>
{renderButtonIcon(!props.result.useNewValue)}
</Button>
</InputGroup.Prepend>
{props.originalField}
</InputGroup>
</Col>
<Form.Label column className="d-lg-none column-label">
{scrapedLabel}
</Form.Label>
<Col lg="6">
<InputGroup>
<InputGroup.Prepend>
<Button
variant="secondary"
onClick={() => handleSelectClick(true)}
>
{renderButtonIcon(props.result.useNewValue)}
</Button>
</InputGroup.Prepend>
{props.newField}
</InputGroup>
{props.newValues}
</Col>
</Row>
</Col>
</Row>
);
};
interface IScrapedInputGroupProps {
isNew?: boolean;
placeholder?: string;
locked?: boolean;
result: ScrapeResult<string>;
onChange?: (value: string) => void;
}
const ScrapedInputGroup: React.FC<IScrapedInputGroupProps> = (props) => {
return (
<FormControl
placeholder={props.placeholder}
value={props.isNew ? props.result.newValue : props.result.originalValue}
readOnly={!props.isNew || props.locked}
onChange={(e) => {
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<string>;
locked?: boolean;
onChange: (value: ScrapeResult<string>) => void;
}
export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
props
) => {
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
className={props.className}
result={props.result}
originalField={
<ScrapedInputGroup
placeholder={props.placeholder || props.title}
result={props.result}
/>
}
newField={
<ScrapedInputGroup
placeholder={props.placeholder || props.title}
result={props.result}
isNew
locked={props.locked}
onChange={(value) =>
props.onChange(props.result.cloneWithValue(value))
}
/>
}
onChange={props.onChange}
/>
);
};
interface IScrapedStringListProps {
isNew?: boolean;
placeholder?: string;
locked?: boolean;
result: ScrapeResult<string[]>;
onChange?: (value: string[]) => void;
}
const ScrapedStringList: React.FC<IScrapedStringListProps> = (props) => {
const value = props.isNew
? props.result.newValue
: props.result.originalValue;
return (
<StringListInput
value={value ?? []}
setValue={(v) => {
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<string[]>;
locked?: boolean;
onChange: (value: ScrapeResult<string[]>) => void;
}
export const ScrapedStringListRow: React.FC<IScrapedStringListRowProps> = (
props
) => {
return (
<ScrapeDialogRow
className="string-list-row"
title={props.title}
field={props.field}
result={props.result}
originalField={
<ScrapedStringList
placeholder={props.placeholder || props.title}
result={props.result}
/>
}
newField={
<ScrapedStringList
placeholder={props.placeholder || props.title}
result={props.result}
isNew
locked={props.locked}
onChange={(value) =>
props.onChange(props.result.cloneWithValue(value))
}
/>
}
onChange={props.onChange}
/>
);
};
const ScrapedTextArea: React.FC<IScrapedInputGroupProps> = (props) => {
return (
<FormControl
as="textarea"
placeholder={props.placeholder}
value={props.isNew ? props.result.newValue : props.result.originalValue}
readOnly={!props.isNew}
onChange={(e) => {
if (props.isNew && props.onChange) {
props.onChange(e.target.value);
}
}}
className="bg-secondary text-white border-secondary scene-description"
/>
);
};
export const ScrapedTextAreaRow: React.FC<IScrapedInputGroupRowProps> = (
props
) => {
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
result={props.result}
originalField={
<ScrapedTextArea
placeholder={props.placeholder || props.title}
result={props.result}
/>
}
newField={
<ScrapedTextArea
placeholder={props.placeholder || props.title}
result={props.result}
isNew
onChange={(value) =>
props.onChange(props.result.cloneWithValue(value))
}
/>
}
onChange={props.onChange}
/>
);
};
interface IScrapedImageProps {
isNew?: boolean;
className?: string;
placeholder?: string;
result: ScrapeResult<string>;
}
const ScrapedImage: React.FC<IScrapedImageProps> = (props) => {
const value = props.isNew
? props.result.newValue
: props.result.originalValue;
if (!value) {
return <></>;
}
return (
<img className={props.className} src={value} alt={props.placeholder} />
);
};
interface IScrapedImageRowProps {
title: string;
field: string;
className?: string;
result: ScrapeResult<string>;
onChange: (value: ScrapeResult<string>) => void;
}
export const ScrapedImageRow: React.FC<IScrapedImageRowProps> = (props) => {
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
result={props.result}
originalField={
<ScrapedImage
result={props.result}
className={props.className}
placeholder={props.title}
/>
}
newField={
<ScrapedImage
result={props.result}
className={props.className}
placeholder={props.title}
isNew
/>
}
onChange={props.onChange}
/>
);
};
interface IScrapedImagesRowProps {
title: string;
field: string;
className?: string;
result: ScrapeResult<string>;
images: string[];
onChange: (value: ScrapeResult<string>) => void;
}
export const ScrapedImagesRow: React.FC<IScrapedImagesRowProps> = (props) => {
const [imageIndex, setImageIndex] = useState(0);
function onSetImageIndex(newIdx: number) {
const ret = props.result.cloneWithValue(props.images[newIdx]);
props.onChange(ret);
setImageIndex(newIdx);
}
return (
<ScrapeDialogRow
title={props.title}
field={props.field}
result={props.result}
originalField={
<ScrapedImage
result={props.result}
className={props.className}
placeholder={props.title}
/>
}
newField={
<div className="image-selection-parent">
<ImageSelector
imageClassName={props.className}
images={props.images}
imageIndex={imageIndex}
setImageIndex={onSetImageIndex}
/>
</div>
}
onChange={props.onChange}
/>
);
};
interface IScrapedCountryRowProps {
title: string;
field: string;
result: ScrapeResult<string>;
onChange: (value: ScrapeResult<string>) => void;
locked?: boolean;
locale?: string;
}
export const ScrapedCountryRow: React.FC<IScrapedCountryRowProps> = ({
title,
field,
result,
onChange,
locked,
locale,
}) => (
<ScrapeDialogRow
title={title}
field={field}
result={result}
originalField={
<FormControl
value={
getCountryByISO(result.originalValue, locale) ?? result.originalValue
}
readOnly
className="bg-secondary text-white border-secondary"
/>
}
newField={
<CountrySelect
value={result.newValue}
disabled={locked}
onChange={(value) => {
if (onChange) {
onChange(result.cloneWithValue(value));
}
}}
showFlag={false}
isClearable={false}
className="flex-grow-1"
/>
}
onChange={onChange}
/>
);

View File

@@ -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<T> {
newValues: T[];
onCreateNew: (value: T) => void;
getName: (value: T) => string;
}
export const NewScrapedObjects = <T,>(props: INewScrapedObjects<T>) => {
const intl = useIntl();
if (props.newValues.length === 0) {
return null;
}
const ret = (
<>
{props.newValues.map((t) => (
<Badge
className="tag-item"
variant="secondary"
key={props.getName(t)}
onClick={() => props.onCreateNew(t)}
>
{props.getName(t)}
<Button className="minimal ml-2">
<Icon className="fa-fw" icon={faPlus} />
</Button>
</Badge>
))}
</>
);
const minCollapseLength = 10;
if (props.newValues!.length >= minCollapseLength) {
const missingText = intl.formatMessage({
id: "dialogs.scrape_results_missing",
});
return (
<CollapseButton text={`${missingText} (${props.newValues!.length})`}>
{ret}
</CollapseButton>
);
}
return ret;
};
interface IScrapedStudioRow {
title: string;
@@ -77,18 +129,20 @@ export const ScrapedStudioRow: React.FC<IScrapedStudioRow> = ({
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 ? (
<NewScrapedObjects
newValues={[newStudio]}
onCreateNew={onCreateNew}
getName={getObjectName}
/>
) : undefined
}
/>
);
};
@@ -125,18 +179,20 @@ export const ScrapedObjectsRow = <T,>(props: IScrapedObjectsRow<T>) => {
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 ? (
<NewScrapedObjects
newValues={newObjects ?? []}
onCreateNew={onCreateNew}
getName={getName}
/>
) : undefined
}
/>
);
};

View File

@@ -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 {

View File

@@ -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",