mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Multiple scene URLs (#3852)
* Add URLs scene relationship * Update unit tests * Update scene edit and details pages * Update scrapers to use urls * Post-process scenes during query scrape * Update UI for URLs * Change urls label
This commit is contained in:
@@ -22,6 +22,7 @@ import {
|
||||
} from "@fortawesome/free-solid-svg-icons";
|
||||
import { getCountryByISO } from "src/utils/country";
|
||||
import { CountrySelect } from "./CountrySelect";
|
||||
import { StringListInput } from "./StringListInput";
|
||||
|
||||
export class ScrapeResult<T> {
|
||||
public newValue?: T;
|
||||
@@ -102,6 +103,7 @@ interface IScrapedFieldProps<T> {
|
||||
|
||||
interface IScrapedRowProps<T, V extends IHasName>
|
||||
extends IScrapedFieldProps<T> {
|
||||
className?: string;
|
||||
title: string;
|
||||
renderOriginalField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
renderNewField: (result: ScrapeResult<T>) => JSX.Element | undefined;
|
||||
@@ -175,7 +177,7 @@ export const ScrapeDialogRow = <T, V extends IHasName>(
|
||||
}
|
||||
|
||||
return (
|
||||
<Row className="px-3 pt-3">
|
||||
<Row className={`px-3 pt-3 ${props.className ?? ""}`}>
|
||||
<Form.Label column lg="3">
|
||||
{props.title}
|
||||
</Form.Label>
|
||||
@@ -276,6 +278,71 @@ export const ScrapedInputGroupRow: React.FC<IScrapedInputGroupRowProps> = (
|
||||
);
|
||||
};
|
||||
|
||||
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;
|
||||
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}
|
||||
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}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
const ScrapedTextArea: React.FC<IScrapedInputGroupProps> = (props) => {
|
||||
return (
|
||||
<FormControl
|
||||
|
||||
@@ -1,18 +1,55 @@
|
||||
import { faMinus } from "@fortawesome/free-solid-svg-icons";
|
||||
import React from "react";
|
||||
import React, { ComponentType } from "react";
|
||||
import { Button, Form, InputGroup } from "react-bootstrap";
|
||||
import { Icon } from "./Icon";
|
||||
|
||||
interface IStringListInputProps {
|
||||
interface IListInputComponentProps {
|
||||
value: string;
|
||||
setValue: (value: string) => void;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
interface IListInputAppendProps {
|
||||
value: string;
|
||||
}
|
||||
|
||||
export interface IStringListInputProps {
|
||||
value: string[];
|
||||
setValue: (value: string[]) => void;
|
||||
inputComponent?: ComponentType<IListInputComponentProps>;
|
||||
appendComponent?: ComponentType<IListInputAppendProps>;
|
||||
placeholder?: string;
|
||||
className?: string;
|
||||
errors?: string;
|
||||
errorIdx?: number[];
|
||||
readOnly?: boolean;
|
||||
}
|
||||
|
||||
export const StringInput: React.FC<IListInputComponentProps> = ({
|
||||
className,
|
||||
placeholder,
|
||||
value,
|
||||
setValue,
|
||||
readOnly = false,
|
||||
}) => {
|
||||
return (
|
||||
<Form.Control
|
||||
className={`text-input ${className ?? ""}`}
|
||||
value={value}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
setValue(e.currentTarget.value)
|
||||
}
|
||||
placeholder={placeholder}
|
||||
readOnly={readOnly}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
||||
const Input = props.inputComponent ?? StringInput;
|
||||
const AppendComponent = props.appendComponent;
|
||||
const values = props.value.concat("");
|
||||
|
||||
function valueChanged(idx: number, value: string) {
|
||||
@@ -37,24 +74,24 @@ export const StringListInput: React.FC<IStringListInputProps> = (props) => {
|
||||
<Form.Group>
|
||||
{values.map((v, i) => (
|
||||
<InputGroup className={props.className} key={i}>
|
||||
<Form.Control
|
||||
className={`text-input ${
|
||||
props.errorIdx?.includes(i) ? "is-invalid" : ""
|
||||
}`}
|
||||
<Input
|
||||
value={v}
|
||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) =>
|
||||
valueChanged(i, e.currentTarget.value)
|
||||
}
|
||||
setValue={(value) => valueChanged(i, value)}
|
||||
placeholder={props.placeholder}
|
||||
className={props.errorIdx?.includes(i) ? "is-invalid" : ""}
|
||||
readOnly={props.readOnly}
|
||||
/>
|
||||
<InputGroup.Append>
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => removeValue(i)}
|
||||
disabled={i === values.length - 1}
|
||||
>
|
||||
<Icon icon={faMinus} />
|
||||
</Button>
|
||||
{AppendComponent && <AppendComponent value={v} />}
|
||||
{!props.readOnly && (
|
||||
<Button
|
||||
variant="danger"
|
||||
onClick={() => removeValue(i)}
|
||||
disabled={i === values.length - 1}
|
||||
>
|
||||
<Icon icon={faMinus} />
|
||||
</Button>
|
||||
)}
|
||||
</InputGroup.Append>
|
||||
</InputGroup>
|
||||
))}
|
||||
|
||||
@@ -4,6 +4,11 @@ import { Button, InputGroup, Form } from "react-bootstrap";
|
||||
import { Icon } from "./Icon";
|
||||
import { FormikHandlers } from "formik";
|
||||
import { faFileDownload } from "@fortawesome/free-solid-svg-icons";
|
||||
import {
|
||||
IStringListInputProps,
|
||||
StringInput,
|
||||
StringListInput,
|
||||
} from "./StringListInput";
|
||||
|
||||
interface IProps {
|
||||
value: string;
|
||||
@@ -43,3 +48,33 @@ export const URLField: React.FC<IProps> = (props: IProps) => {
|
||||
</InputGroup>
|
||||
);
|
||||
};
|
||||
|
||||
interface IURLListProps extends IStringListInputProps {
|
||||
onScrapeClick(url: string): void;
|
||||
urlScrapable(url: string): boolean;
|
||||
}
|
||||
|
||||
export const URLListInput: React.FC<IURLListProps> = (
|
||||
listProps: IURLListProps
|
||||
) => {
|
||||
const intl = useIntl();
|
||||
const { onScrapeClick, urlScrapable } = listProps;
|
||||
return (
|
||||
<StringListInput
|
||||
{...listProps}
|
||||
placeholder={intl.formatMessage({ id: "url" })}
|
||||
inputComponent={StringInput}
|
||||
appendComponent={(props) => (
|
||||
<Button
|
||||
className="scrape-url-button text-input"
|
||||
variant="secondary"
|
||||
onClick={() => onScrapeClick(props.value)}
|
||||
disabled={!props.value || !urlScrapable(props.value)}
|
||||
title={intl.formatMessage({ id: "actions.scrape" })}
|
||||
>
|
||||
<Icon icon={faFileDownload} />
|
||||
</Button>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -441,3 +441,7 @@ div.react-datepicker {
|
||||
right: 0;
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.string-list-row .input-group {
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user