mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Use formik for scene edit (#1429)
* Use formik for scene edit panel * Fix unsetting rating * Disable save if not dirty * Movie image fixes
This commit is contained in:
@@ -9,6 +9,7 @@ import {
|
||||
} from "react-bootstrap";
|
||||
import Mousetrap from "mousetrap";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import * as yup from "yup";
|
||||
import {
|
||||
queryScrapeScene,
|
||||
queryScrapeSceneURL,
|
||||
@@ -28,9 +29,11 @@ import {
|
||||
ImageInput,
|
||||
} from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import { ImageUtils, FormUtils, EditableTextUtils, TextUtils } from "src/utils";
|
||||
import { ImageUtils, FormUtils, TextUtils } from "src/utils";
|
||||
import { MovieSelect } from "src/components/Shared/Select";
|
||||
import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
|
||||
import { useFormik } from "formik";
|
||||
import { Prompt } from "react-router";
|
||||
import { SceneMovieTable } from "./SceneMovieTable";
|
||||
import { RatingStars } from "./RatingStars";
|
||||
import { SceneScrapeDialog } from "./SceneScrapeDialog";
|
||||
|
||||
@@ -38,6 +41,7 @@ interface IProps {
|
||||
scene: GQL.SceneDataFragment;
|
||||
isVisible: boolean;
|
||||
onDelete: () => void;
|
||||
onUpdate?: () => void;
|
||||
}
|
||||
|
||||
export const SceneEditPanel: React.FC<IProps> = ({
|
||||
@@ -46,37 +50,12 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
onDelete,
|
||||
}) => {
|
||||
const Toast = useToast();
|
||||
const [title, setTitle] = useState<string>(scene.title ?? "");
|
||||
const [details, setDetails] = useState<string>(scene.details ?? "");
|
||||
const [url, setUrl] = useState<string>(scene.url ?? "");
|
||||
const [date, setDate] = useState<string>(scene.date ?? "");
|
||||
const [rating, setRating] = useState<number | undefined>(
|
||||
scene.rating ?? undefined
|
||||
);
|
||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
||||
scene.galleries.map((g) => ({
|
||||
id: g.id,
|
||||
title: g.title ?? TextUtils.fileNameFromPath(g.path ?? ""),
|
||||
}))
|
||||
);
|
||||
const [studioId, setStudioId] = useState<string | undefined>(
|
||||
scene.studio?.id
|
||||
);
|
||||
const [performerIds, setPerformerIds] = useState<string[]>(
|
||||
scene.performers.map((p) => p.id)
|
||||
);
|
||||
const [movieIds, setMovieIds] = useState<string[]>(
|
||||
scene.movies.map((m) => m.movie.id)
|
||||
);
|
||||
const [
|
||||
movieSceneIndexes,
|
||||
setMovieSceneIndexes,
|
||||
] = useState<MovieSceneIndexMap>(
|
||||
new Map(scene.movies.map((m) => [m.movie.id, m.scene_index ?? undefined]))
|
||||
);
|
||||
const [tagIds, setTagIds] = useState<string[]>(scene.tags.map((t) => t.id));
|
||||
const [coverImage, setCoverImage] = useState<string>();
|
||||
const [stashIDs, setStashIDs] = useState<GQL.StashIdInput[]>(scene.stash_ids);
|
||||
|
||||
const Scrapers = useListSceneScrapers();
|
||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||
@@ -94,10 +73,60 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
const [updateScene] = useSceneUpdate();
|
||||
|
||||
const schema = yup.object({
|
||||
title: yup.string().optional().nullable(),
|
||||
details: yup.string().optional().nullable(),
|
||||
url: yup.string().optional().nullable(),
|
||||
date: yup.string().optional().nullable(),
|
||||
rating: yup.number().optional().nullable(),
|
||||
gallery_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
studio_id: yup.string().optional().nullable(),
|
||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
movies: yup
|
||||
.object({
|
||||
movie_id: yup.string().required(),
|
||||
scene_index: yup.string().optional().nullable(),
|
||||
})
|
||||
.optional()
|
||||
.nullable(),
|
||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||
cover_image: yup.string().optional().nullable(),
|
||||
stash_ids: yup.mixed<GQL.StashIdInput>().optional().nullable(),
|
||||
});
|
||||
|
||||
const initialValues = {
|
||||
title: scene.title ?? "",
|
||||
details: scene.details ?? "",
|
||||
url: scene.url ?? "",
|
||||
date: scene.date ?? "",
|
||||
rating: scene.rating ?? null,
|
||||
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
||||
studio_id: scene.studio?.id,
|
||||
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
||||
movies: (scene.movies ?? []).map((m) => {
|
||||
return { movie_id: m.movie.id, scene_index: m.scene_index };
|
||||
}),
|
||||
tag_ids: (scene.tags ?? []).map((t) => t.id),
|
||||
cover_image: undefined,
|
||||
stash_ids: scene.stash_ids ?? undefined,
|
||||
};
|
||||
|
||||
type InputValues = typeof initialValues;
|
||||
|
||||
const formik = useFormik({
|
||||
initialValues,
|
||||
validationSchema: schema,
|
||||
onSubmit: (values) => onSave(getSceneInput(values)),
|
||||
});
|
||||
|
||||
function setRating(v: number) {
|
||||
formik.setFieldValue("rating", v);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isVisible) {
|
||||
Mousetrap.bind("s s", () => {
|
||||
onSave();
|
||||
formik.handleSubmit();
|
||||
});
|
||||
Mousetrap.bind("d d", () => {
|
||||
onDelete();
|
||||
@@ -146,86 +175,47 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
setQueryableScrapers(newQueryableScrapers);
|
||||
}, [Scrapers, stashConfig]);
|
||||
|
||||
useEffect(() => {
|
||||
let changed = false;
|
||||
const newMap: MovieSceneIndexMap = new Map();
|
||||
if (movieIds) {
|
||||
movieIds.forEach((id) => {
|
||||
if (!movieSceneIndexes.has(id)) {
|
||||
changed = true;
|
||||
newMap.set(id, undefined);
|
||||
} else {
|
||||
newMap.set(id, movieSceneIndexes.get(id));
|
||||
}
|
||||
});
|
||||
|
||||
if (!changed) {
|
||||
movieSceneIndexes.forEach((_v, id) => {
|
||||
if (!newMap.has(id)) {
|
||||
// id was removed
|
||||
changed = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (changed) {
|
||||
setMovieSceneIndexes(newMap);
|
||||
}
|
||||
}
|
||||
}, [movieIds, movieSceneIndexes]);
|
||||
|
||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||
|
||||
function getSceneInput(): GQL.SceneUpdateInput {
|
||||
function getSceneInput(input: InputValues): GQL.SceneUpdateInput {
|
||||
return {
|
||||
id: scene.id,
|
||||
title,
|
||||
details,
|
||||
url,
|
||||
date,
|
||||
rating: rating ?? null,
|
||||
gallery_ids: galleries.map((g) => g.id),
|
||||
studio_id: studioId ?? null,
|
||||
performer_ids: performerIds,
|
||||
movies: makeMovieInputs(),
|
||||
tag_ids: tagIds,
|
||||
cover_image: coverImage,
|
||||
stash_ids: stashIDs.map((s) => ({
|
||||
stash_id: s.stash_id,
|
||||
endpoint: s.endpoint,
|
||||
})),
|
||||
...input,
|
||||
};
|
||||
}
|
||||
|
||||
function makeMovieInputs(): GQL.SceneMovieInput[] | undefined {
|
||||
if (!movieIds) {
|
||||
return undefined;
|
||||
}
|
||||
function setMovieIds(movieIds: string[]) {
|
||||
const existingMovies = formik.values.movies;
|
||||
|
||||
let ret = movieIds.map((id) => {
|
||||
const r: GQL.SceneMovieInput = {
|
||||
movie_id: id,
|
||||
const newMovies = movieIds.map((m) => {
|
||||
const existing = existingMovies.find((mm) => mm.movie_id === m);
|
||||
if (existing) {
|
||||
return existing;
|
||||
}
|
||||
|
||||
return {
|
||||
movie_id: m,
|
||||
};
|
||||
return r;
|
||||
});
|
||||
|
||||
ret = ret.map((r) => {
|
||||
return { scene_index: movieSceneIndexes.get(r.movie_id), ...r };
|
||||
});
|
||||
|
||||
return ret;
|
||||
formik.setFieldValue("movies", newMovies);
|
||||
}
|
||||
|
||||
async function onSave() {
|
||||
async function onSave(input: GQL.SceneUpdateInput) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await updateScene({
|
||||
variables: {
|
||||
input: getSceneInput(),
|
||||
input: {
|
||||
...input,
|
||||
rating: input.rating ?? null,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (result.data?.sceneUpdate) {
|
||||
Toast.success({ content: "Updated scene" });
|
||||
// clear the cover image so that it doesn't appear dirty
|
||||
formik.resetForm({ values: formik.values });
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
@@ -234,8 +224,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||
setStashIDs(
|
||||
stashIDs.filter(
|
||||
formik.setFieldValue(
|
||||
"stash_ids",
|
||||
formik.values.stash_ids.filter(
|
||||
(s) =>
|
||||
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
||||
)
|
||||
@@ -245,9 +236,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
function renderTableMovies() {
|
||||
return (
|
||||
<SceneMovieTable
|
||||
movieSceneIndexes={movieSceneIndexes}
|
||||
movieScenes={formik.values.movies}
|
||||
onUpdate={(items) => {
|
||||
setMovieSceneIndexes(items);
|
||||
formik.setFieldValue("movies", items);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
@@ -255,7 +246,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
function onImageLoad(imageData: string) {
|
||||
setCoverImagePreview(imageData);
|
||||
setCoverImage(imageData);
|
||||
formik.setFieldValue("cover_image", imageData);
|
||||
}
|
||||
|
||||
function onCoverImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||
@@ -287,7 +278,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
async function onScrapeClicked(scraper: GQL.Scraper) {
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await queryScrapeScene(scraper.id, getSceneInput());
|
||||
const result = await queryScrapeScene(
|
||||
scraper.id,
|
||||
getSceneInput(formik.values)
|
||||
);
|
||||
if (!result.data || !result.data.scrapeScene) {
|
||||
Toast.success({
|
||||
content: "No scenes found",
|
||||
@@ -328,7 +322,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
return;
|
||||
}
|
||||
|
||||
const currentScene = getSceneInput();
|
||||
const currentScene = getSceneInput(formik.values);
|
||||
if (!currentScene.cover_image) {
|
||||
currentScene.cover_image = scene.paths.screenshot;
|
||||
}
|
||||
@@ -423,23 +417,23 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
updatedScene: GQL.ScrapedSceneDataFragment
|
||||
) {
|
||||
if (updatedScene.title) {
|
||||
setTitle(updatedScene.title);
|
||||
formik.setFieldValue("title", updatedScene.title);
|
||||
}
|
||||
|
||||
if (updatedScene.details) {
|
||||
setDetails(updatedScene.details);
|
||||
formik.setFieldValue("details", updatedScene.details);
|
||||
}
|
||||
|
||||
if (updatedScene.date) {
|
||||
setDate(updatedScene.date);
|
||||
formik.setFieldValue("date", updatedScene.date);
|
||||
}
|
||||
|
||||
if (updatedScene.url) {
|
||||
setUrl(updatedScene.url);
|
||||
formik.setFieldValue("url", updatedScene.url);
|
||||
}
|
||||
|
||||
if (updatedScene.studio && updatedScene.studio.stored_id) {
|
||||
setStudioId(updatedScene.studio.stored_id);
|
||||
formik.setFieldValue("studio_id", updatedScene.studio.stored_id);
|
||||
}
|
||||
|
||||
if (updatedScene.performers && updatedScene.performers.length > 0) {
|
||||
@@ -449,7 +443,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
if (idPerfs.length > 0) {
|
||||
const newIds = idPerfs.map((p) => p.stored_id);
|
||||
setPerformerIds(newIds as string[]);
|
||||
formik.setFieldValue("performer_ids", newIds as string[]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -471,24 +465,24 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
|
||||
if (idTags.length > 0) {
|
||||
const newIds = idTags.map((p) => p.stored_id);
|
||||
setTagIds(newIds as string[]);
|
||||
formik.setFieldValue("tag_ids", newIds as string[]);
|
||||
}
|
||||
}
|
||||
|
||||
if (updatedScene.image) {
|
||||
// image is a base64 string
|
||||
setCoverImage(updatedScene.image);
|
||||
formik.setFieldValue("cover_image", updatedScene.image);
|
||||
setCoverImagePreview(updatedScene.image);
|
||||
}
|
||||
}
|
||||
|
||||
async function onScrapeSceneURL() {
|
||||
if (!url) {
|
||||
if (!formik.values.url) {
|
||||
return;
|
||||
}
|
||||
setIsLoading(true);
|
||||
try {
|
||||
const result = await queryScrapeSceneURL(url);
|
||||
const result = await queryScrapeSceneURL(formik.values.url);
|
||||
if (!result.data || !result.data.scrapeSceneURL) {
|
||||
return;
|
||||
}
|
||||
@@ -501,7 +495,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
}
|
||||
|
||||
function maybeRenderScrapeButton() {
|
||||
if (!url || !urlScrapable(url)) {
|
||||
if (!formik.values.url || !urlScrapable(formik.values.url)) {
|
||||
return undefined;
|
||||
}
|
||||
return (
|
||||
@@ -515,219 +509,253 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||
return (
|
||||
<Form.Group controlId={title} as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title,
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder={placeholder ?? title}
|
||||
{...formik.getFieldProps(field)}
|
||||
isInvalid={!!formik.getFieldMeta(field).error}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
if (isLoading) return <LoadingIndicator />;
|
||||
|
||||
return (
|
||||
<div id="scene-edit-details">
|
||||
<Prompt
|
||||
when={formik.dirty}
|
||||
message="Unsaved changes. Are you sure you want to leave?"
|
||||
/>
|
||||
|
||||
{maybeRenderScrapeDialog()}
|
||||
<div className="form-container row px-3 pt-3">
|
||||
<div className="col-6 edit-buttons mb-3 pl-0">
|
||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className="edit-button"
|
||||
variant="danger"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||
<div className="form-container row px-3 pt-3">
|
||||
<div className="col-6 edit-buttons mb-3 pl-0">
|
||||
<Button
|
||||
className="edit-button"
|
||||
variant="primary"
|
||||
disabled={!formik.dirty}
|
||||
onClick={() => formik.submitForm()}
|
||||
>
|
||||
Save
|
||||
</Button>
|
||||
<Button
|
||||
className="edit-button"
|
||||
variant="danger"
|
||||
onClick={() => onDelete()}
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</div>
|
||||
<Col xs={6} className="text-right">
|
||||
{maybeRenderStashboxQueryButton()}
|
||||
{renderScraperMenu()}
|
||||
</Col>
|
||||
</div>
|
||||
<Col xs={6} className="text-right">
|
||||
{maybeRenderStashboxQueryButton()}
|
||||
{renderScraperMenu()}
|
||||
</Col>
|
||||
</div>
|
||||
<div className="form-container row px-3">
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
{FormUtils.renderInputGroup({
|
||||
title: "Title",
|
||||
value: title,
|
||||
onChange: setTitle,
|
||||
isEditing: true,
|
||||
})}
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
<Col xs={3} className="pr-0 url-label">
|
||||
<Form.Label className="col-form-label">URL</Form.Label>
|
||||
<div className="float-right scrape-button-container">
|
||||
{maybeRenderScrapeButton()}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
{EditableTextUtils.renderInputGroup({
|
||||
title: "URL",
|
||||
value: url,
|
||||
onChange: setUrl,
|
||||
isEditing: true,
|
||||
})}
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{FormUtils.renderInputGroup({
|
||||
title: "Date",
|
||||
value: date,
|
||||
isEditing: true,
|
||||
onChange: setDate,
|
||||
placeholder: "YYYY-MM-DD",
|
||||
})}
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={rating}
|
||||
onSetRating={(value) => setRating(value)}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="galleries" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Galleries",
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<GallerySelect
|
||||
galleries={galleries}
|
||||
onSelect={(items) => setGalleries(items)}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
setStudioId(items.length > 0 ? items[0]?.id : undefined)
|
||||
}
|
||||
ids={studioId ? [studioId] : []}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="performers" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Performers",
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
xl: 12,
|
||||
},
|
||||
})}
|
||||
<Col sm={9} xl={12}>
|
||||
<PerformerSelect
|
||||
isMulti
|
||||
onSelect={(items) =>
|
||||
setPerformerIds(items.map((item) => item.id))
|
||||
}
|
||||
ids={performerIds}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="moviesScenes" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Movies/Scenes",
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
xl: 12,
|
||||
},
|
||||
})}
|
||||
<Col sm={9} xl={12}>
|
||||
<MovieSelect
|
||||
isMulti
|
||||
onSelect={(items) => setMovieIds(items.map((item) => item.id))}
|
||||
ids={movieIds}
|
||||
/>
|
||||
{renderTableMovies()}
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Tags",
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
xl: 12,
|
||||
},
|
||||
})}
|
||||
<Col sm={9} xl={12}>
|
||||
<TagSelect
|
||||
isMulti
|
||||
onSelect={(items) => setTagIds(items.map((item) => item.id))}
|
||||
ids={tagIds}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="details">
|
||||
<Form.Label>StashIDs</Form.Label>
|
||||
<ul className="pl-0">
|
||||
{stashIDs.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
) : (
|
||||
stashID.stash_id
|
||||
);
|
||||
return (
|
||||
<li key={stashID.stash_id} className="row no-gutters">
|
||||
<Button
|
||||
variant="danger"
|
||||
className="mr-2 py-0"
|
||||
title="Delete StashID"
|
||||
onClick={() => removeStashID(stashID)}
|
||||
>
|
||||
<Icon icon="trash-alt" />
|
||||
</Button>
|
||||
{link}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Form.Group>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
<Form.Group controlId="details">
|
||||
<Form.Label>Details</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="scene-description text-input"
|
||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
setDetails(newValue.currentTarget.value)
|
||||
}
|
||||
value={details}
|
||||
/>
|
||||
</Form.Group>
|
||||
<div>
|
||||
<Form.Group controlId="cover">
|
||||
<Form.Label>Cover Image</Form.Label>
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
<img
|
||||
className="scene-cover"
|
||||
src={coverImagePreview}
|
||||
alt="Scene cover"
|
||||
<div className="form-container row px-3">
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
{renderTextField("title", "Title")}
|
||||
<Form.Group controlId="url" as={Row}>
|
||||
<Col xs={3} className="pr-0 url-label">
|
||||
<Form.Label className="col-form-label">URL</Form.Label>
|
||||
<div className="float-right scrape-button-container">
|
||||
{maybeRenderScrapeButton()}
|
||||
</div>
|
||||
</Col>
|
||||
<Col xs={9}>
|
||||
<Form.Control
|
||||
className="text-input"
|
||||
placeholder="URL"
|
||||
{...formik.getFieldProps("url")}
|
||||
isInvalid={!!formik.getFieldMeta("url").error}
|
||||
/>
|
||||
)}
|
||||
<ImageInput
|
||||
isEditing
|
||||
onImageChange={onCoverImageChange}
|
||||
onImageURL={onImageLoad}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
{renderTextField("date", "Date", "YYYY-MM-DD")}
|
||||
<Form.Group controlId="rating" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Rating",
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<RatingStars
|
||||
value={formik.values.rating ?? undefined}
|
||||
onSetRating={(value) =>
|
||||
formik.setFieldValue("rating", value ?? null)
|
||||
}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="galleries" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Galleries",
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<GallerySelect
|
||||
galleries={galleries}
|
||||
onSelect={(items) => setGalleries(items)}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="studio" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Studio",
|
||||
})}
|
||||
<Col xs={9}>
|
||||
<StudioSelect
|
||||
onSelect={(items) =>
|
||||
formik.setFieldValue(
|
||||
"studio_id",
|
||||
items.length > 0 ? items[0]?.id : undefined
|
||||
)
|
||||
}
|
||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="performers" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Performers",
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
xl: 12,
|
||||
},
|
||||
})}
|
||||
<Col sm={9} xl={12}>
|
||||
<PerformerSelect
|
||||
isMulti
|
||||
onSelect={(items) =>
|
||||
formik.setFieldValue(
|
||||
"performer_ids",
|
||||
items.map((item) => item.id)
|
||||
)
|
||||
}
|
||||
ids={formik.values.performer_ids}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="moviesScenes" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Movies/Scenes",
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
xl: 12,
|
||||
},
|
||||
})}
|
||||
<Col sm={9} xl={12}>
|
||||
<MovieSelect
|
||||
isMulti
|
||||
onSelect={(items) =>
|
||||
setMovieIds(items.map((item) => item.id))
|
||||
}
|
||||
ids={formik.values.movies.map((m) => m.movie_id)}
|
||||
/>
|
||||
{renderTableMovies()}
|
||||
</Col>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group controlId="tags" as={Row}>
|
||||
{FormUtils.renderLabel({
|
||||
title: "Tags",
|
||||
labelProps: {
|
||||
column: true,
|
||||
sm: 3,
|
||||
xl: 12,
|
||||
},
|
||||
})}
|
||||
<Col sm={9} xl={12}>
|
||||
<TagSelect
|
||||
isMulti
|
||||
onSelect={(items) =>
|
||||
formik.setFieldValue(
|
||||
"tag_ids",
|
||||
items.map((item) => item.id)
|
||||
)
|
||||
}
|
||||
ids={formik.values.tag_ids}
|
||||
/>
|
||||
</Col>
|
||||
</Form.Group>
|
||||
<Form.Group controlId="details">
|
||||
<Form.Label>StashIDs</Form.Label>
|
||||
<ul className="pl-0">
|
||||
{formik.values.stash_ids.map((stashID) => {
|
||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||
const link = base ? (
|
||||
<a
|
||||
href={`${base}scenes/${stashID.stash_id}`}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
{stashID.stash_id}
|
||||
</a>
|
||||
) : (
|
||||
stashID.stash_id
|
||||
);
|
||||
return (
|
||||
<li key={stashID.stash_id} className="row no-gutters">
|
||||
<Button
|
||||
variant="danger"
|
||||
className="mr-2 py-0"
|
||||
title="Delete StashID"
|
||||
onClick={() => removeStashID(stashID)}
|
||||
>
|
||||
<Icon icon="trash-alt" />
|
||||
</Button>
|
||||
{link}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</Form.Group>
|
||||
</div>
|
||||
<div className="col-12 col-lg-6 col-xl-12">
|
||||
<Form.Group controlId="details">
|
||||
<Form.Label>Details</Form.Label>
|
||||
<Form.Control
|
||||
as="textarea"
|
||||
className="scene-description text-input"
|
||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||
formik.setFieldValue("details", newValue.currentTarget.value)
|
||||
}
|
||||
value={formik.values.details}
|
||||
/>
|
||||
</Form.Group>
|
||||
<div>
|
||||
<Form.Group controlId="cover">
|
||||
<Form.Label>Cover Image</Form.Label>
|
||||
{imageEncoding ? (
|
||||
<LoadingIndicator message="Encoding image..." />
|
||||
) : (
|
||||
<img
|
||||
className="scene-cover"
|
||||
src={coverImagePreview}
|
||||
alt="Scene cover"
|
||||
/>
|
||||
)}
|
||||
<ImageInput
|
||||
isEditing
|
||||
onImageChange={onCoverImageChange}
|
||||
onImageURL={onImageLoad}
|
||||
/>
|
||||
</Form.Group>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user