Files
stash/ui/v2.5/src/components/Images/ImageDetails/ImageScrapeDialog.tsx
WithoutPants 51999135be Add SFW content mode option (#6262)
* Use more neutral language for content
* Add sfw mode setting
* Make configuration context mandatory
* Add sfw class when sfw mode active
* Hide nsfw performer fields in sfw mode
* Hide nsfw sort options
* Hide nsfw filter/sort options in sfw mode
* Replace o-count with like counter in sfw mode
* Use sfw label for o-counter filter in sfw mode
* Use likes instead of o-count in sfw mode in other places
* Rename sfw mode to sfw content mode
* Use sfw image for default performers in sfw mode
* Document SFW content mode
* Add SFW mode setting to setup
* Clarify README
* Change wording of sfw mode description
* Handle configuration loading error correctly
* Hide age in performer cards
2025-11-18 11:13:35 +11:00

236 lines
6.6 KiB
TypeScript

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";
import {
ObjectListScrapeResult,
ObjectScrapeResult,
ScrapeResult,
} from "src/components/Shared/ScrapeDialog/scrapeResult";
import {
ScrapedPerformersRow,
ScrapedStudioRow,
} from "src/components/Shared/ScrapeDialog/ScrapedObjectsRow";
import { sortStoredIdObjects } from "src/utils/data";
import { Performer } from "src/components/Performers/PerformerSelect";
import {
useCreateScrapedPerformer,
useCreateScrapedStudio,
} from "src/components/Shared/ScrapeDialog/createObjects";
import { uniq } from "lodash-es";
import { Tag } from "src/components/Tags/TagSelect";
import { Studio } from "src/components/Studios/StudioSelect";
import { useScrapedTags } from "src/components/Shared/ScrapeDialog/scrapedTags";
interface IImageScrapeDialogProps {
image: Partial<GQL.ImageUpdateInput>;
imageStudio: Studio | null;
imageTags: Tag[];
imagePerformers: Performer[];
scraped: GQL.ScrapedImage;
onClose: (scrapedImage?: GQL.ScrapedImage) => void;
}
export const ImageScrapeDialog: React.FC<IImageScrapeDialogProps> = ({
image,
imageStudio,
imageTags,
imagePerformers,
scraped,
onClose,
}) => {
const intl = useIntl();
const [title, setTitle] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(image.title, scraped.title)
);
const [code, setCode] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(image.code, scraped.code)
);
const [urls, setURLs] = useState<ScrapeResult<string[]>>(
new ScrapeResult<string[]>(
image.urls,
scraped.urls
? uniq((image.urls ?? []).concat(scraped.urls ?? []))
: undefined
)
);
const [date, setDate] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(image.date, scraped.date)
);
const [photographer, setPhotographer] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(image.photographer, scraped.photographer)
);
const [studio, setStudio] = useState<ObjectScrapeResult<GQL.ScrapedStudio>>(
new ObjectScrapeResult<GQL.ScrapedStudio>(
imageStudio
? {
stored_id: imageStudio.id,
name: imageStudio.name,
}
: undefined,
scraped.studio?.stored_id ? scraped.studio : undefined
)
);
const [newStudio, setNewStudio] = useState<GQL.ScrapedStudio | undefined>(
scraped.studio && !scraped.studio.stored_id ? scraped.studio : undefined
);
const [performers, setPerformers] = useState<
ObjectListScrapeResult<GQL.ScrapedPerformer>
>(
new ObjectListScrapeResult<GQL.ScrapedPerformer>(
sortStoredIdObjects(
imagePerformers.map((p) => ({
stored_id: p.id,
name: p.name,
}))
),
sortStoredIdObjects(scraped.performers ?? undefined)
)
);
const [newPerformers, setNewPerformers] = useState<GQL.ScrapedPerformer[]>(
scraped.performers?.filter((t) => !t.stored_id) ?? []
);
const { tags, newTags, scrapedTagsRow } = useScrapedTags(
imageTags,
scraped.tags
);
const [details, setDetails] = useState<ScrapeResult<string>>(
new ScrapeResult<string>(image.details, scraped.details)
);
const createNewStudio = useCreateScrapedStudio({
scrapeResult: studio,
setScrapeResult: setStudio,
setNewObject: setNewStudio,
});
const createNewPerformer = useCreateScrapedPerformer({
scrapeResult: performers,
setScrapeResult: setPerformers,
newObjects: newPerformers,
setNewObjects: setNewPerformers,
});
// don't show the dialog if nothing was scraped
if (
[
title,
code,
urls,
date,
photographer,
studio,
performers,
tags,
details,
].every((r) => !r.scraped) &&
!newStudio &&
newPerformers.length === 0 &&
newTags.length === 0
) {
onClose();
return <></>;
}
function makeNewScrapedItem(): GQL.ScrapedImageDataFragment {
const newStudioValue = studio.getNewValue();
return {
title: title.getNewValue(),
code: code.getNewValue(),
urls: urls.getNewValue(),
date: date.getNewValue(),
photographer: photographer.getNewValue(),
studio: newStudioValue,
performers: performers.getNewValue(),
tags: tags.getNewValue(),
details: details.getNewValue(),
};
}
function renderScrapeRows() {
return (
<>
<ScrapedInputGroupRow
field="title"
title={intl.formatMessage({ id: "title" })}
result={title}
onChange={(value) => setTitle(value)}
/>
<ScrapedInputGroupRow
field="code"
title={intl.formatMessage({ id: "scene_code" })}
result={code}
onChange={(value) => setCode(value)}
/>
<ScrapedStringListRow
field="urls"
title={intl.formatMessage({ id: "urls" })}
result={urls}
onChange={(value) => setURLs(value)}
/>
<ScrapedInputGroupRow
field="date"
title={intl.formatMessage({ id: "date" })}
placeholder="YYYY-MM-DD"
result={date}
onChange={(value) => setDate(value)}
/>
<ScrapedInputGroupRow
field="photographer"
title={intl.formatMessage({ id: "photographer" })}
result={photographer}
onChange={(value) => setPhotographer(value)}
/>
<ScrapedStudioRow
field="studio"
title={intl.formatMessage({ id: "studios" })}
result={studio}
onChange={(value) => setStudio(value)}
newStudio={newStudio}
onCreateNew={createNewStudio}
/>
<ScrapedPerformersRow
field="performers"
title={intl.formatMessage({ id: "performers" })}
result={performers}
onChange={(value) => setPerformers(value)}
newObjects={newPerformers}
onCreateNew={createNewPerformer}
/>
{scrapedTagsRow}
<ScrapedTextAreaRow
field="details"
title={intl.formatMessage({ id: "details" })}
result={details}
onChange={(value) => setDetails(value)}
/>
</>
);
}
return (
<ScrapeDialog
title={intl.formatMessage(
{ id: "dialogs.scrape_entity_title" },
{ entity_type: intl.formatMessage({ id: "image" }) }
)}
renderScrapeRows={renderScrapeRows}
onClose={(apply) => {
onClose(apply ? makeNewScrapedItem() : undefined);
}}
/>
);
};