diff --git a/pkg/api/resolver_model_movie.go b/pkg/api/resolver_model_movie.go index afd82ab8a..be105eb9e 100644 --- a/pkg/api/resolver_model_movie.go +++ b/pkg/api/resolver_model_movie.go @@ -89,6 +89,24 @@ func (r *movieResolver) FrontImagePath(ctx context.Context, obj *models.Movie) ( } func (r *movieResolver) BackImagePath(ctx context.Context, obj *models.Movie) (*string, error) { + // don't return any thing if there is no back image + var img []byte + if err := r.withReadTxn(ctx, func(repo models.ReaderRepository) error { + var err error + img, err = repo.Movie().GetBackImage(obj.ID) + if err != nil { + return err + } + + return nil + }); err != nil { + return nil, err + } + + if img == nil { + return nil, nil + } + baseURL, _ := ctx.Value(BaseURLCtxKey).(string) backimagePath := urlbuilders.NewMovieURLBuilder(baseURL, obj).GetMovieBackImageURL() return &backimagePath, nil diff --git a/ui/v2.5/src/components/Changelog/versions/v070.md b/ui/v2.5/src/components/Changelog/versions/v070.md index d7f2c5451..c1ab489c7 100644 --- a/ui/v2.5/src/components/Changelog/versions/v070.md +++ b/ui/v2.5/src/components/Changelog/versions/v070.md @@ -2,6 +2,7 @@ * Added scene queue. ### 🎨 Improvements +* Improve Movie UI. * Change performer text query to search by name and alias only. ### 🐛 Bug fixes diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index bd95459af..b2d4fdf38 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -1,5 +1,5 @@ -import React, { useEffect, useState, useCallback } from "react"; -import { useIntl } from "react-intl"; +import React, { useEffect, useState } from "react"; +import cx from "classnames"; import Mousetrap from "mousetrap"; import * as GQL from "src/core/generated-graphql"; import { @@ -7,29 +7,19 @@ import { useMovieUpdate, useMovieCreate, useMovieDestroy, - queryScrapeMovieURL, - useListMovieScrapers, } from "src/core/StashService"; import { useParams, useHistory } from "react-router-dom"; import { DetailsEditNavbar, LoadingIndicator, Modal, - StudioSelect, - Icon, } from "src/components/Shared"; import { useToast } from "src/hooks"; -import { Table, Form, Modal as BSModal, Button } from "react-bootstrap"; -import { - TableUtils, - ImageUtils, - EditableTextUtils, - TextUtils, - DurationUtils, -} from "src/utils"; -import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; +import { Modal as BSModal, Button } from "react-bootstrap"; +import { ImageUtils } from "src/utils"; import { MovieScenesPanel } from "./MovieScenesPanel"; -import { MovieScrapeDialog } from "./MovieScrapeDialog"; +import { MovieDetailsPanel } from "./MovieDetailsPanel"; +import { MovieEditPanel } from "./MovieEditPanel"; interface IMovieParams { id?: string; @@ -53,111 +43,32 @@ export const Movie: React.FC = () => { const [backImage, setBackImage] = useState( undefined ); - const [name, setName] = useState(undefined); - const [aliases, setAliases] = useState(undefined); - const [duration, setDuration] = useState(undefined); - const [date, setDate] = useState(undefined); - const [rating, setRating] = useState(undefined); - const [studioId, setStudioId] = useState(); - const [director, setDirector] = useState(undefined); - const [synopsis, setSynopsis] = useState(undefined); - const [url, setUrl] = useState(undefined); // Movie state - const [movie, setMovie] = useState>({}); - const [imagePreview, setImagePreview] = useState( - undefined - ); - const [backimagePreview, setBackImagePreview] = useState( - undefined - ); - const [imageClipboard, setImageClipboard] = useState( undefined ); // Network state const { data, error, loading } = useFindMovie(id); + const movie = data?.findMovie; + const [isLoading, setIsLoading] = useState(false); const [updateMovie] = useMovieUpdate(); - const [createMovie] = useMovieCreate(getMovieInput() as GQL.MovieCreateInput); - const [deleteMovie] = useMovieDestroy( - getMovieInput() as GQL.MovieDestroyInput - ); - - const Scrapers = useListMovieScrapers(); - const [scrapedMovie, setScrapedMovie] = useState< - GQL.ScrapedMovie | undefined - >(); - - const intl = useIntl(); + const [createMovie] = useMovieCreate(); + const [deleteMovie] = useMovieDestroy({ id }); // set up hotkeys useEffect(() => { - if (isEditing) { - Mousetrap.bind("r 0", () => setRating(NaN)); - Mousetrap.bind("r 1", () => setRating(1)); - Mousetrap.bind("r 2", () => setRating(2)); - Mousetrap.bind("r 3", () => setRating(3)); - Mousetrap.bind("r 4", () => setRating(4)); - Mousetrap.bind("r 5", () => setRating(5)); - // Mousetrap.bind("u", (e) => { - // setStudioFocus() - // e.preventDefault(); - // }); - Mousetrap.bind("s s", () => onSave()); - } - Mousetrap.bind("e", () => setIsEditing(true)); Mousetrap.bind("d d", () => onDelete()); return () => { - if (isEditing) { - Mousetrap.unbind("r 0"); - Mousetrap.unbind("r 1"); - Mousetrap.unbind("r 2"); - Mousetrap.unbind("r 3"); - Mousetrap.unbind("r 4"); - Mousetrap.unbind("r 5"); - // Mousetrap.unbind("u"); - Mousetrap.unbind("s s"); - } - Mousetrap.unbind("e"); Mousetrap.unbind("d d"); }; }); - function updateMovieEditState(state: Partial) { - setName(state.name ?? undefined); - setAliases(state.aliases ?? undefined); - setDuration(state.duration ?? undefined); - setDate(state.date ?? undefined); - setRating(state.rating ?? undefined); - setStudioId(state?.studio?.id ?? undefined); - setDirector(state.director ?? undefined); - setSynopsis(state.synopsis ?? undefined); - setUrl(state.url ?? undefined); - } - - const updateMovieData = useCallback( - (movieData: Partial) => { - setFrontImage(undefined); - setBackImage(undefined); - updateMovieEditState(movieData); - setImagePreview(movieData.front_image_path ?? undefined); - setBackImagePreview(movieData.back_image_path ?? undefined); - setMovie(movieData); - }, - [] - ); - - useEffect(() => { - if (data && data.findMovie) { - updateMovieData(data.findMovie); - } - }, [data, updateMovieData]); - function showImageAlert(imageData: string) { setImageClipboard(imageData); setIsImageAlertOpen(true); @@ -165,10 +76,8 @@ export const Movie: React.FC = () => { function setImageFromClipboard(isFrontImage: boolean) { if (isFrontImage) { - setImagePreview(imageClipboard); setFrontImage(imageClipboard); } else { - setBackImagePreview(imageClipboard); setBackImage(imageClipboard); } @@ -176,16 +85,6 @@ export const Movie: React.FC = () => { setIsImageAlertOpen(false); } - function onBackImageLoad(imageData: string) { - setBackImagePreview(imageData); - setBackImage(imageData); - } - - function onFrontImageLoad(imageData: string) { - setImagePreview(imageData); - setFrontImage(imageData); - } - const encodingImage = ImageUtils.usePasteImage(showImageAlert, isEditing); if (!isNew && !isEditing) { @@ -195,41 +94,41 @@ export const Movie: React.FC = () => { } } - function getMovieInput() { - const input: Partial = { - name, - aliases, - duration, - date, - rating: rating ?? null, - studio_id: studioId ?? null, - director, - synopsis, - url, + function getMovieInput( + input: Partial + ) { + const ret: Partial = { + ...input, front_image: frontImage, back_image: backImage, }; if (!isNew) { - (input as GQL.MovieUpdateInput).id = id; + (ret as GQL.MovieUpdateInput).id = id; } - return input; + return ret; } - async function onSave() { + async function onSave( + input: Partial + ) { try { + setIsLoading(true); + if (!isNew) { const result = await updateMovie({ variables: { - input: getMovieInput() as GQL.MovieUpdateInput, + input: getMovieInput(input) as GQL.MovieUpdateInput, }, }); if (result.data?.movieUpdate) { - updateMovieData(result.data.movieUpdate); setIsEditing(false); + history.push(`/movies/${result.data.movieUpdate.id}`); } } else { - const result = await createMovie(); + const result = await createMovie({ + variables: getMovieInput(input) as GQL.MovieCreateInput, + }); if (result.data?.movieCreate?.id) { history.push(`/movies/${result.data.movieCreate.id}`); setIsEditing(false); @@ -237,31 +136,29 @@ export const Movie: React.FC = () => { } } catch (e) { Toast.error(e); + } finally { + setIsLoading(false); } } async function onDelete() { try { + setIsLoading(true); await deleteMovie(); } catch (e) { Toast.error(e); + } finally { + setIsLoading(false); } // redirect to movies page history.push(`/movies`); } - function onFrontImageChange(event: React.FormEvent) { - ImageUtils.onImageChange(event, onFrontImageLoad); - } - - function onBackImageChange(event: React.FormEvent) { - ImageUtils.onImageChange(event, onBackImageLoad); - } - function onToggleEdit() { setIsEditing(!isEditing); - updateMovieData(movie); + setFrontImage(undefined); + setBackImage(undefined); } function renderDeleteAlert() { @@ -272,7 +169,7 @@ export const Movie: React.FC = () => { accept={{ text: "Delete", variant: "danger", onClick: onDelete }} cancel={{ onClick: () => setIsDeleteAlertOpen(false) }} > -

Are you sure you want to delete {name ?? "movie"}?

+

Are you sure you want to delete {movie?.name ?? "movie"}?

); } @@ -314,153 +211,42 @@ export const Movie: React.FC = () => { ); } - function updateMovieEditStateFromScraper( - state: Partial - ) { - if (state.name) { - setName(state.name); - } - - if (state.aliases) { - setAliases(state.aliases ?? undefined); - } - - if (state.duration) { - setDuration(DurationUtils.stringToSeconds(state.duration) ?? undefined); - } - - if (state.date) { - setDate(state.date ?? undefined); - } - - if (state.studio && state.studio.id) { - setStudioId(state.studio.id ?? undefined); - } - - if (state.director) { - setDirector(state.director ?? undefined); - } - if (state.synopsis) { - setSynopsis(state.synopsis ?? undefined); - } - if (state.url) { - setUrl(state.url ?? undefined); - } - - // image is a base64 string - // #404: don't overwrite image if it has been modified by the user - // overwrite if not new since it came from a dialog - // otherwise follow existing behaviour - if ( - (!isNew || frontImage === undefined) && - (state as GQL.ScrapedMovieDataFragment).front_image !== undefined - ) { - const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image; - setFrontImage(imageStr ?? undefined); - setImagePreview(imageStr ?? undefined); - } - - if ( - (!isNew || backImage === undefined) && - (state as GQL.ScrapedMovieDataFragment).back_image !== undefined - ) { - const imageStr = (state as GQL.ScrapedMovieDataFragment).back_image; - setBackImage(imageStr ?? undefined); - setBackImagePreview(imageStr ?? undefined); - } - } - - async function onScrapeMovieURL() { - if (!url) return; - setIsLoading(true); - - try { - const result = await queryScrapeMovieURL(url); - if (!result.data || !result.data.scrapeMovieURL) { - return; + function renderFrontImage() { + let image = movie?.front_image_path; + if (isEditing) { + if (frontImage === null) { + image = `${image}&default=true`; + } else if (frontImage) { + image = frontImage; } + } - // if this is a new movie, just dump the data - if (isNew) { - updateMovieEditStateFromScraper(result.data.scrapeMovieURL); - } else { - setScrapedMovie(result.data.scrapeMovieURL); + if (image) { + return ( +
+ Front Cover +
+ ); + } + } + + function renderBackImage() { + let image = movie?.back_image_path; + if (isEditing) { + if (backImage === null) { + image = undefined; + } else if (backImage) { + image = backImage; } - } catch (e) { - Toast.error(e); - } finally { - setIsLoading(false); - } - } - - function urlScrapable(scrapedUrl: string) { - return ( - !!scrapedUrl && - (Scrapers?.data?.listMovieScrapers ?? []).some((s) => - (s?.movie?.urls ?? []).some((u) => scrapedUrl.includes(u)) - ) - ); - } - - function maybeRenderScrapeButton() { - if (!url || !isEditing || !urlScrapable(url)) { - return undefined; - } - return ( - - ); - } - - function maybeRenderScrapeDialog() { - if (!scrapedMovie) { - return; } - const currentMovie = getMovieInput(); - - // Get image paths for scrape gui - currentMovie.front_image = movie.front_image_path; - currentMovie.back_image = movie.back_image_path; - - return ( - { - onScrapeDialogClosed(m); - }} - /> - ); - } - - function onScrapeDialogClosed(p?: GQL.ScrapedMovieDataFragment) { - if (p) { - updateMovieEditStateFromScraper(p); + if (image) { + return ( +
+ Back Cover +
+ ); } - setScrapedMovie(undefined); - } - - function onClearFrontImage() { - setFrontImage(null); - setImagePreview( - movie.front_image_path - ? `${movie.front_image_path}?default=true` - : undefined - ); - } - - function onClearBackImage() { - setBackImage(null); - setBackImagePreview( - movie.back_image_path - ? `${movie.back_image_path}?default=true` - : undefined - ); } if (isLoading) return ; @@ -468,125 +254,55 @@ export const Movie: React.FC = () => { // TODO: CSS class return (
-
- {isNew &&

Add Movie

} +
{encodingImage ? ( ) : ( - <> - {name} - {name} - +
+ {renderFrontImage()} + {renderBackImage()} +
)}
- - - {TableUtils.renderInputGroup({ - title: "Name", - value: name ?? "", - isEditing: !!isEditing, - onChange: setName, - })} - {TableUtils.renderInputGroup({ - title: "Aliases", - value: aliases, - isEditing, - onChange: setAliases, - })} - {TableUtils.renderDurationInput({ - title: "Duration", - value: duration ? duration.toString() : "", - isEditing, - onChange: (value: string | undefined) => - setDuration(value ? Number.parseInt(value, 10) : undefined), - })} - {TableUtils.renderInputGroup({ - title: `Date ${isEditing ? "(YYYY-MM-DD)" : ""}`, - value: isEditing ? date : TextUtils.formatDate(intl, date), - isEditing, - onChange: setDate, - })} - - - - - {TableUtils.renderInputGroup({ - title: "Director", - value: director, - isEditing, - onChange: setDirector, - })} - - - - - -
Studio - - setStudioId(items.length > 0 ? items[0]?.id : undefined) - } - ids={studioId ? [studioId] : []} - /> -
Rating - setRating(value)} - /> -
- - - URL {maybeRenderScrapeButton()} -
- {EditableTextUtils.renderInputGroup({ - isEditing, - onChange: setUrl, - value: url, - url: TextUtils.sanitiseURL(url), - })} -
-
- - - Synopsis - ) => - setSynopsis(newValue.currentTarget.value) - } - value={synopsis} + {!isEditing && movie ? ( + <> + + {/* HACK - this is also rendered in the MovieEditPanel */} + {}} + onImageChange={() => {}} + onDelete={onDelete} + /> + + ) : ( + - - - + )}
- {!isNew && ( -
+ + {!isNew && movie && ( +
)} {renderDeleteAlert()} {renderImageAlert()} - {maybeRenderScrapeDialog()}
); }; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx new file mode 100644 index 000000000..5f3c27fe0 --- /dev/null +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieDetailsPanel.tsx @@ -0,0 +1,81 @@ +import React from "react"; +import { useIntl } from "react-intl"; +import * as GQL from "src/core/generated-graphql"; +import { DurationUtils, TextUtils } from "src/utils"; +import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; +import { TextField, URLField } from "src/utils/field"; + +interface IMovieDetailsPanel { + movie: Partial; +} + +export const MovieDetailsPanel: React.FC = ({ movie }) => { + // Network state + const intl = useIntl(); + + function maybeRenderAliases() { + if (movie.aliases) { + return ( +
+ Also known as + {movie.aliases} +
+ ); + } + } + + function renderRatingField() { + if (!movie.rating) { + return; + } + + return ( +
+
Rating
+
+ +
+
+ ); + } + + // TODO: CSS class + return ( +
+
+

{movie.name}

+
+ + {maybeRenderAliases()} + +
+ + + + + + {renderRatingField()} + + + + +
+
+ ); +}; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx new file mode 100644 index 000000000..4c9a71aa0 --- /dev/null +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -0,0 +1,418 @@ +import React, { useEffect, useState } from "react"; +import * as GQL from "src/core/generated-graphql"; +import * as yup from "yup"; +import Mousetrap from "mousetrap"; +import { + queryScrapeMovieURL, + useListMovieScrapers, +} from "src/core/StashService"; +import { + LoadingIndicator, + StudioSelect, + Icon, + DetailsEditNavbar, + DurationInput, +} from "src/components/Shared"; +import { useToast } from "src/hooks"; +import { Form, Button, Col, Row, InputGroup } from "react-bootstrap"; +import { DurationUtils, ImageUtils } from "src/utils"; +import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars"; +import { useFormik } from "formik"; +import { Prompt } from "react-router-dom"; +import { MovieScrapeDialog } from "./MovieScrapeDialog"; + +interface IMovieEditPanel { + movie?: Partial; + onSubmit: ( + movie: Partial + ) => void; + onCancel: () => void; + onDelete: () => void; + setFrontImage: (image?: string | null) => void; + setBackImage: (image?: string | null) => void; +} + +export const MovieEditPanel: React.FC = ({ + movie, + onSubmit, + onCancel, + onDelete, + setFrontImage, + setBackImage, +}) => { + const Toast = useToast(); + + const isNew = movie === undefined; + + const [isLoading, setIsLoading] = useState(false); + + const Scrapers = useListMovieScrapers(); + const [scrapedMovie, setScrapedMovie] = useState< + GQL.ScrapedMovie | undefined + >(); + + const labelXS = 3; + const labelXL = 3; + const fieldXS = 9; + const fieldXL = 9; + + const schema = yup.object({ + name: yup.string().required(), + aliases: yup.string().optional().nullable(), + duration: yup.string().optional().nullable(), + date: yup + .string() + .optional() + .nullable() + .matches(/^\d{4}-\d{2}-\d{2}$/), + rating: yup.number().optional().nullable(), + studio_id: yup.string().optional().nullable(), + director: yup.string().optional().nullable(), + synopsis: yup.string().optional().nullable(), + url: yup.string().optional().nullable(), + }); + + const initialValues = { + name: movie?.name, + aliases: movie?.aliases, + duration: movie?.duration, + date: movie?.date, + rating: movie?.rating, + studio_id: movie?.studio?.id, + director: movie?.director, + synopsis: movie?.synopsis, + url: movie?.url, + }; + + type InputValues = typeof initialValues; + + const formik = useFormik({ + initialValues, + validationSchema: schema, + onSubmit: (values) => onSubmit(getMovieInput(values)), + }); + + function setRating(v: number) { + formik.setFieldValue("rating", v); + } + + // set up hotkeys + useEffect(() => { + Mousetrap.bind("r 0", () => setRating(NaN)); + Mousetrap.bind("r 1", () => setRating(1)); + Mousetrap.bind("r 2", () => setRating(2)); + Mousetrap.bind("r 3", () => setRating(3)); + Mousetrap.bind("r 4", () => setRating(4)); + Mousetrap.bind("r 5", () => setRating(5)); + // Mousetrap.bind("u", (e) => { + // setStudioFocus() + // e.preventDefault(); + // }); + Mousetrap.bind("s s", () => formik.handleSubmit()); + + return () => { + Mousetrap.unbind("r 0"); + Mousetrap.unbind("r 1"); + Mousetrap.unbind("r 2"); + Mousetrap.unbind("r 3"); + Mousetrap.unbind("r 4"); + Mousetrap.unbind("r 5"); + // Mousetrap.unbind("u"); + Mousetrap.unbind("s s"); + }; + }); + + function getMovieInput(values: InputValues) { + const input: Partial = { + ...values, + rating: values.rating ?? null, + studio_id: values.studio_id ?? null, + }; + + if (movie && movie.id) { + (input as GQL.MovieUpdateInput).id = movie.id; + } + return input; + } + + function updateMovieEditStateFromScraper( + state: Partial + ) { + if (state.name) { + formik.setFieldValue("name", state.name); + } + + if (state.aliases) { + formik.setFieldValue("aliases", state.aliases ?? undefined); + } + + if (state.duration) { + formik.setFieldValue( + "duration", + DurationUtils.stringToSeconds(state.duration) ?? undefined + ); + } + + if (state.date) { + formik.setFieldValue("date", state.date ?? undefined); + } + + if (state.studio && state.studio.id) { + formik.setFieldValue("studio_id", state.studio.id ?? undefined); + } + + if (state.director) { + formik.setFieldValue("director", state.director ?? undefined); + } + if (state.synopsis) { + formik.setFieldValue("synopsis", state.synopsis ?? undefined); + } + if (state.url) { + formik.setFieldValue("url", state.url ?? undefined); + } + + const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image; + setFrontImage(imageStr ?? undefined); + + const backImageStr = (state as GQL.ScrapedMovieDataFragment).back_image; + setBackImage(backImageStr ?? undefined); + } + + async function onScrapeMovieURL() { + const { url } = formik.values; + if (!url) return; + setIsLoading(true); + + try { + const result = await queryScrapeMovieURL(url); + if (!result.data || !result.data.scrapeMovieURL) { + return; + } + + // if this is a new movie, just dump the data + if (isNew) { + updateMovieEditStateFromScraper(result.data.scrapeMovieURL); + } else { + setScrapedMovie(result.data.scrapeMovieURL); + } + } catch (e) { + Toast.error(e); + } finally { + setIsLoading(false); + } + } + + function urlScrapable(scrapedUrl: string) { + return ( + !!scrapedUrl && + (Scrapers?.data?.listMovieScrapers ?? []).some((s) => + (s?.movie?.urls ?? []).some((u) => scrapedUrl.includes(u)) + ) + ); + } + + function maybeRenderScrapeButton() { + const { url } = formik.values; + if (!url || !urlScrapable(url)) { + return undefined; + } + return ( + + ); + } + + function maybeRenderScrapeDialog() { + if (!scrapedMovie) { + return; + } + + const currentMovie = getMovieInput(formik.values); + + // Get image paths for scrape gui + currentMovie.front_image = movie?.front_image_path; + currentMovie.back_image = movie?.back_image_path; + + return ( + { + onScrapeDialogClosed(m); + }} + /> + ); + } + + function onScrapeDialogClosed(p?: GQL.ScrapedMovieDataFragment) { + if (p) { + updateMovieEditStateFromScraper(p); + } + setScrapedMovie(undefined); + } + + function onFrontImageChange(event: React.FormEvent) { + ImageUtils.onImageChange(event, setFrontImage); + } + + function onBackImageChange(event: React.FormEvent) { + ImageUtils.onImageChange(event, setBackImage); + } + + if (isLoading) return ; + + const isEditing = true; + + function renderTextField(field: string, title: string) { + return ( + + + {title} + + + + + + ); + } + + // TODO: CSS class + return ( +
+ {isNew &&

Add Movie

} + + + +
+ + + Name + + + + + {formik.errors.name} + + + + + {renderTextField("aliases", "Aliases")} + + + + Duration + + + { + formik.setFieldValue("duration", valueAsNumber); + }} + /> + + + + {renderTextField("date", "Date (YYYY-MM-DD)")} + + + + Studio + + + + formik.setFieldValue( + "studio_id", + items.length > 0 ? items[0]?.id : undefined + ) + } + ids={formik.values.studio_id ? [formik.values.studio_id] : []} + /> + + + + {renderTextField("director", "Director")} + + + + Rating + + + formik.setFieldValue("rating", value)} + /> + + + + + + URL + + + + + {maybeRenderScrapeButton()} + + + + + + + Synopsis + + + + + +
+ + formik.handleSubmit()} + onImageChange={onFrontImageChange} + onImageChangeURL={setFrontImage} + onClearImage={() => { + setFrontImage(null); + }} + onBackImageChange={onBackImageChange} + onBackImageChangeURL={setBackImage} + onClearBackImage={() => { + setBackImage(null); + }} + onDelete={onDelete} + /> + + {maybeRenderScrapeDialog()} +
+ ); +}; diff --git a/ui/v2.5/src/components/Movies/styles.scss b/ui/v2.5/src/components/Movies/styles.scss index 45fb9c14b..c87122a55 100644 --- a/ui/v2.5/src/components/Movies/styles.scss +++ b/ui/v2.5/src/components/Movies/styles.scss @@ -18,3 +18,21 @@ width: 100%; } } + +.movie-images { + align-items: center; + display: flex; + flex-direction: row; + justify-content: space-evenly; + margin: 1rem; + max-width: 100%; + + .movie-image-container { + margin: 1rem; + } + + img { + max-width: 100%; + object-fit: contain; + } +} diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx index b2ed6f860..77838220c 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneScrapeDialog.tsx @@ -317,7 +317,7 @@ export const SceneScrapeDialog: React.FC = ( const [createStudio] = useStudioCreate({ name: "" }); const [createPerformer] = usePerformerCreate(); - const [createMovie] = useMovieCreate({ name: "" }); + const [createMovie] = useMovieCreate(); const [createTag] = useTagCreate({ name: "" }); const Toast = useToast(); diff --git a/ui/v2.5/src/core/StashService.ts b/ui/v2.5/src/core/StashService.ts index 10832462d..b35da8123 100644 --- a/ui/v2.5/src/core/StashService.ts +++ b/ui/v2.5/src/core/StashService.ts @@ -602,9 +602,8 @@ export const movieMutationImpactedQueries = [ GQL.AllMoviesForFilterDocument, ]; -export const useMovieCreate = (input: GQL.MovieCreateInput) => +export const useMovieCreate = () => GQL.useMovieCreateMutation({ - variables: input, update: deleteCache([ GQL.FindMoviesDocument, GQL.AllMoviesForFilterDocument, diff --git a/ui/v2.5/src/utils/field.tsx b/ui/v2.5/src/utils/field.tsx index 9833d87c9..14fd5b9e9 100644 --- a/ui/v2.5/src/utils/field.tsx +++ b/ui/v2.5/src/utils/field.tsx @@ -29,7 +29,7 @@ export const URLField: React.FC = ({ name, value, url }) => { return null; } return ( -
+
{name}:
{url ? (