mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Gallery URLs (#4114)
* Initial backend changes * Fix unit tests * UI changes * Fix missing URL filters
This commit is contained in:
@@ -25,7 +25,7 @@ import {
|
||||
} from "src/components/Shared/Select";
|
||||
import { Icon } from "src/components/Shared/Icon";
|
||||
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 { useFormik } from "formik";
|
||||
import FormUtils from "src/utils/form";
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
Performer,
|
||||
PerformerSelect,
|
||||
} from "src/components/Performers/PerformerSelect";
|
||||
import { yupDateString, yupUniqueStringList } from "src/utils/yup";
|
||||
|
||||
interface IProps {
|
||||
gallery: Partial<GQL.GalleryDataFragment>;
|
||||
@@ -84,20 +85,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const schema = yup.object({
|
||||
title: titleRequired ? yup.string().required() : 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(),
|
||||
@@ -108,7 +97,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const initialValues = {
|
||||
title: gallery?.title ?? "",
|
||||
url: gallery?.url ?? "",
|
||||
urls: gallery?.urls ?? [],
|
||||
date: gallery?.date ?? "",
|
||||
rating100: gallery?.rating100 ?? null,
|
||||
studio_id: gallery?.studio?.id ?? null,
|
||||
@@ -313,8 +302,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
formik.setFieldValue("date", galleryData.date);
|
||||
}
|
||||
|
||||
if (galleryData.url) {
|
||||
formik.setFieldValue("url", galleryData.url);
|
||||
if (galleryData.urls) {
|
||||
formik.setFieldValue("url", galleryData.urls);
|
||||
}
|
||||
|
||||
if (galleryData.studio?.stored_id) {
|
||||
@@ -351,13 +340,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
async function onScrapeGalleryURL() {
|
||||
if (!formik.values.url) {
|
||||
async function onScrapeGalleryURL(url: string) {
|
||||
if (!url) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await queryScrapeGalleryURL(formik.values.url);
|
||||
const result = await queryScrapeGalleryURL(url);
|
||||
if (!result || !result.data || !result.data.scrapeGalleryURL) {
|
||||
return;
|
||||
}
|
||||
@@ -392,6 +381,14 @@ export const GalleryEditPanel: 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="gallery-edit-details">
|
||||
<Prompt
|
||||
@@ -428,18 +425,20 @@ export const GalleryEditPanel: 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={onScrapeGalleryURL}
|
||||
<URLListInput
|
||||
value={formik.values.urls ?? []}
|
||||
setValue={(value) => formik.setFieldValue("urls", value)}
|
||||
errors={urlsErrorMsg}
|
||||
errorIdx={urlsErrorIdx}
|
||||
onScrapeClick={(url) => onScrapeGalleryURL(url)}
|
||||
urlScrapable={urlScrapable}
|
||||
isInvalid={!!formik.getFieldMeta("url").error}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
@@ -7,7 +7,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import { mutateGallerySetPrimaryFile } 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 {
|
||||
folder?: Pick<GQL.Folder, "id" | "path">;
|
||||
@@ -147,12 +147,7 @@ export const GalleryFileInfoPanel: React.FC<IGalleryFileInfoPanelProps> = (
|
||||
return (
|
||||
<>
|
||||
<dl className="container gallery-file-info details-list">
|
||||
<URLField
|
||||
id="media_info.downloaded_from"
|
||||
url={props.gallery.url}
|
||||
value={props.gallery.url}
|
||||
truncate
|
||||
/>
|
||||
<URLsField id="urls" urls={props.gallery.urls} truncate />
|
||||
</dl>
|
||||
|
||||
{filesPanel}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as GQL from "src/core/generated-graphql";
|
||||
import {
|
||||
ScrapeDialog,
|
||||
ScrapedInputGroupRow,
|
||||
ScrapedStringListRow,
|
||||
ScrapedTextAreaRow,
|
||||
} from "src/components/Shared/ScrapeDialog/ScrapeDialog";
|
||||
import clone from "lodash-es/clone";
|
||||
@@ -23,6 +24,7 @@ import {
|
||||
useCreateScrapedStudio,
|
||||
useCreateScrapedTag,
|
||||
} from "src/components/Shared/ScrapeDialog/createObjects";
|
||||
import { uniq } from "lodash-es";
|
||||
|
||||
interface IGalleryScrapeDialogProps {
|
||||
gallery: Partial<GQL.GalleryUpdateInput>;
|
||||
@@ -36,29 +38,32 @@ interface IHasStoredID {
|
||||
stored_id?: string | null;
|
||||
}
|
||||
|
||||
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
props: IGalleryScrapeDialogProps
|
||||
) => {
|
||||
export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = ({
|
||||
gallery,
|
||||
galleryPerformers,
|
||||
scraped,
|
||||
onClose,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const [title, setTitle] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.gallery.title, props.scraped.title)
|
||||
new ScrapeResult<string>(gallery.title, scraped.title)
|
||||
);
|
||||
const [url, setURL] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.gallery.url, props.scraped.url)
|
||||
);
|
||||
const [date, setDate] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.gallery.date, props.scraped.date)
|
||||
);
|
||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(
|
||||
props.gallery.studio_id,
|
||||
props.scraped.studio?.stored_id
|
||||
const [urls, setURLs] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
gallery.urls,
|
||||
scraped.urls
|
||||
? uniq((gallery.urls ?? []).concat(scraped.urls ?? []))
|
||||
: undefined
|
||||
)
|
||||
);
|
||||
const [date, setDate] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(gallery.date, scraped.date)
|
||||
);
|
||||
const [studio, setStudio] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(gallery.studio_id, scraped.studio?.stored_id)
|
||||
);
|
||||
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
|
||||
props.scraped.studio && !props.scraped.studio.stored_id
|
||||
? props.scraped.studio
|
||||
: undefined
|
||||
scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
|
||||
);
|
||||
|
||||
function mapStoredIdObjects(
|
||||
@@ -104,30 +109,30 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
>(
|
||||
new ObjectListScrapeResult<GQL.ScrapedPerformer>(
|
||||
sortStoredIdObjects(
|
||||
props.galleryPerformers.map((p) => ({
|
||||
galleryPerformers.map((p) => ({
|
||||
stored_id: p.id,
|
||||
name: p.name,
|
||||
}))
|
||||
),
|
||||
sortStoredIdObjects(props.scraped.performers ?? undefined)
|
||||
sortStoredIdObjects(scraped.performers ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
|
||||
props.scraped.performers?.filter((t) => !t.stored_id) ?? []
|
||||
scraped.performers?.filter((t) => !t.stored_id) ?? []
|
||||
);
|
||||
|
||||
const [tags, setTags] = useState<ScrapeResult<string[]>>(
|
||||
new ScrapeResult<string[]>(
|
||||
sortIdList(props.gallery.tag_ids),
|
||||
mapStoredIdObjects(props.scraped.tags ?? undefined)
|
||||
sortIdList(gallery.tag_ids),
|
||||
mapStoredIdObjects(scraped.tags ?? undefined)
|
||||
)
|
||||
);
|
||||
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>(
|
||||
props.scraped.tags?.filter((t) => !t.stored_id) ?? []
|
||||
scraped.tags?.filter((t) => !t.stored_id) ?? []
|
||||
);
|
||||
|
||||
const [details, setDetails] = useState<ScrapeResult<string>>(
|
||||
new ScrapeResult<string>(props.gallery.details, props.scraped.details)
|
||||
new ScrapeResult<string>(gallery.details, scraped.details)
|
||||
);
|
||||
|
||||
const createNewStudio = useCreateScrapedStudio({
|
||||
@@ -152,14 +157,14 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
|
||||
// don't show the dialog if nothing was scraped
|
||||
if (
|
||||
[title, url, date, studio, performers, tags, details].every(
|
||||
[title, urls, date, studio, performers, tags, details].every(
|
||||
(r) => !r.scraped
|
||||
) &&
|
||||
!newStudio &&
|
||||
newPerformers.length === 0 &&
|
||||
newTags.length === 0
|
||||
) {
|
||||
props.onClose();
|
||||
onClose();
|
||||
return <></>;
|
||||
}
|
||||
|
||||
@@ -168,7 +173,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
|
||||
return {
|
||||
title: title.getNewValue(),
|
||||
url: url.getNewValue(),
|
||||
urls: urls.getNewValue(),
|
||||
date: date.getNewValue(),
|
||||
studio: newStudioValue
|
||||
? {
|
||||
@@ -195,10 +200,10 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
result={title}
|
||||
onChange={(value) => setTitle(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title={intl.formatMessage({ id: "url" })}
|
||||
result={url}
|
||||
onChange={(value) => setURL(value)}
|
||||
<ScrapedStringListRow
|
||||
title={intl.formatMessage({ id: "urls" })}
|
||||
result={urls}
|
||||
onChange={(value) => setURLs(value)}
|
||||
/>
|
||||
<ScrapedInputGroupRow
|
||||
title={intl.formatMessage({ id: "date" })}
|
||||
@@ -244,7 +249,7 @@ export const GalleryScrapeDialog: React.FC<IGalleryScrapeDialogProps> = (
|
||||
)}
|
||||
renderScrapeRows={renderScrapeRows}
|
||||
onClose={(apply) => {
|
||||
props.onClose(apply ? makeNewScrapedItem() : undefined);
|
||||
onClose(apply ? makeNewScrapedItem() : undefined);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user