mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Multiple image URLs (#4000)
* Backend changes - ported from scene impl * Front end changes * Refactor URL mutation code
This commit is contained in:
@@ -6,7 +6,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
import { TagSelect, StudioSelect } from "src/components/Shared/Select";
|
||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||
import { URLField } from "src/components/Shared/URLField";
|
||||
import { URLListInput } from "src/components/Shared/URLField";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import FormUtils from "src/utils/form";
|
||||
import { useFormik } from "formik";
|
||||
@@ -16,6 +16,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
|
||||
import {
|
||||
Performer,
|
||||
PerformerSelect,
|
||||
@@ -46,20 +47,8 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().ensure(),
|
||||
url: yup.string().ensure(),
|
||||
date: yup
|
||||
.string()
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
urls: yupUniqueStringList("urls"),
|
||||
date: yupDateString(intl),
|
||||
rating100: yup.number().nullable().defined(),
|
||||
studio_id: yup.string().required().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).defined(),
|
||||
@@ -68,7 +57,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const initialValues = {
|
||||
title: image.title ?? "",
|
||||
url: image?.url ?? "",
|
||||
urls: image?.urls ?? [],
|
||||
date: image?.date ?? "",
|
||||
rating100: image.rating100 ?? null,
|
||||
studio_id: image.studio?.id ?? null,
|
||||
@@ -162,6 +151,14 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
|
||||
const urlsErrors = Array.isArray(formik.errors.urls)
|
||||
? formik.errors.urls[0]
|
||||
: formik.errors.urls;
|
||||
const urlsErrorMsg = urlsErrors
|
||||
? intl.formatMessage({ id: "validation.urls_must_be_unique" })
|
||||
: undefined;
|
||||
const urlsErrorIdx = urlsErrors?.split(" ").map((e) => parseInt(e));
|
||||
|
||||
return (
|
||||
<div id="image-edit-details">
|
||||
<Prompt
|
||||
@@ -192,20 +189,18 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||
<div className="form-container row px-3">
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
{renderTextField("title", intl.formatMessage({ id: "title" }))}
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
<Form.Group controlId="urls" as={Row}>
|
||||
<Col xs={3} className="pr-0 url-label">
|
||||
<Form.Label className="col-form-label">
|
||||
<FormattedMessage id="url" />
|
||||
<FormattedMessage id="urls" />
|
||||
</Form.Label>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<URLField
|
||||
{...formik.getFieldProps("url")}
|
||||
onScrapeClick={() => {}}
|
||||
urlScrapable={() => {
|
||||
return false;
|
||||
}}
|
||||
isInvalid={!!formik.getFieldMeta("url").error}
|
||||
<URLListInput
|
||||
value={formik.values.urls ?? []}
|
||||
setValue={(value) => formik.setFieldValue("urls", value)}
|
||||
errors={urlsErrorMsg}
|
||||
errorIdx={urlsErrorIdx}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { mutateImageSetPrimaryFile } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks/Toast";
|
||||
import TextUtils from "src/utils/text";
|
||||
import { TextField, URLField } from "src/utils/field";
|
||||
import { TextField, URLField, URLsField } from "src/utils/field";
|
||||
|
||||
interface IFileInfoPanelProps {
|
||||
file: GQL.ImageFileDataFragment | GQL.VideoFileDataFragment;
|
||||
@@ -120,20 +120,11 @@ export const ImageFileInfoPanel: React.FC<IImageFileInfoPanelProps> = (
|
||||
if (props.image.visual_files.length === 1) {
|
||||
return (
|
||||
<>
|
||||
<FileInfoPanel file={props.image.visual_files[0]} />
|
||||
<dl className="container image-file-info details-list">
|
||||
<URLsField id="urls" urls={props.image.urls} truncate />
|
||||
</dl>
|
||||
|
||||
{props.image.url ? (
|
||||
<dl className="container image-file-info details-list">
|
||||
<URLField
|
||||
id="media_info.downloaded_from"
|
||||
url={TextUtils.sanitiseURL(props.image.url)}
|
||||
value={TextUtils.domainFromURL(props.image.url)}
|
||||
truncate
|
||||
/>
|
||||
</dl>
|
||||
) : (
|
||||
""
|
||||
)}
|
||||
<FileInfoPanel file={props.image.visual_files[0]} />
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@ import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||
import { lazyComponent } from "src/utils/lazyComponent";
|
||||
import isEqual from "lodash-es/isEqual";
|
||||
import { DateInput } from "src/components/Shared/DateInput";
|
||||
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
|
||||
import {
|
||||
Performer,
|
||||
PerformerSelect,
|
||||
@@ -114,38 +115,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
const schema = yup.object({
|
||||
title: yup.string().ensure(),
|
||||
code: yup.string().ensure(),
|
||||
urls: yup
|
||||
.array(yup.string().required())
|
||||
.defined()
|
||||
.test({
|
||||
name: "unique",
|
||||
test: (value) => {
|
||||
const dupes = value
|
||||
.map((e, i, a) => {
|
||||
if (a.indexOf(e) !== i) {
|
||||
return String(i - 1);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.filter((e) => e !== null) as string[];
|
||||
if (dupes.length === 0) return true;
|
||||
return new yup.ValidationError(dupes.join(" "), value, "urls");
|
||||
},
|
||||
}),
|
||||
date: yup
|
||||
.string()
|
||||
.ensure()
|
||||
.test({
|
||||
name: "date",
|
||||
test: (value) => {
|
||||
if (!value) return true;
|
||||
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||
if (Number.isNaN(Date.parse(value))) return false;
|
||||
return true;
|
||||
},
|
||||
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||
}),
|
||||
urls: yupUniqueStringList("urls"),
|
||||
date: yupDateString(intl),
|
||||
director: yup.string().ensure(),
|
||||
rating100: yup.number().nullable().defined(),
|
||||
gallery_ids: yup.array(yup.string().required()).defined(),
|
||||
|
||||
@@ -50,8 +50,8 @@ export const URLField: React.FC<IProps> = (props: IProps) => {
|
||||
};
|
||||
|
||||
interface IURLListProps extends IStringListInputProps {
|
||||
onScrapeClick(url: string): void;
|
||||
urlScrapable(url: string): boolean;
|
||||
onScrapeClick?: (url: string) => void;
|
||||
urlScrapable?: (url: string) => boolean;
|
||||
}
|
||||
|
||||
export const URLListInput: React.FC<IURLListProps> = (
|
||||
@@ -64,17 +64,23 @@ export const URLListInput: React.FC<IURLListProps> = (
|
||||
{...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>
|
||||
)}
|
||||
appendComponent={(props) => {
|
||||
if (!onScrapeClick || !urlScrapable) {
|
||||
return <></>;
|
||||
}
|
||||
|
||||
return (
|
||||
<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>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user