diff --git a/ui/v2.5/src/components/Changelog/versions/v080.md b/ui/v2.5/src/components/Changelog/versions/v080.md index c702f8137..ed99750b4 100644 --- a/ui/v2.5/src/components/Changelog/versions/v080.md +++ b/ui/v2.5/src/components/Changelog/versions/v080.md @@ -7,6 +7,7 @@ * Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364)) ### 🎨 Improvements +* Prompt when leaving scene edit page with unsaved changed. ([#1429](https://github.com/stashapp/stash/pull/1429)) * Make multi-set mode buttons more obvious in multi-edit dialog. ([#1435](https://github.com/stashapp/stash/pull/1435)) * Filter modifiers and sort by options are now sorted alphabetically. ([#1406](https://github.com/stashapp/stash/pull/1406)) * Add `CreatedAt` and `UpdatedAt` (and `FileModTime` where applicable) to API objects. ([#1421](https://github.com/stashapp/stash/pull/1421)) @@ -16,6 +17,7 @@ * Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378)) ### 🐛 Bug fixes +* Fix clearing Performer and Movie ratings not working. ([#1429](https://github.com/stashapp/stash/pull/1429)) * Fix scraper date parser failing when parsing time. ([#1431](https://github.com/stashapp/stash/pull/1431)) * Fix quotes in filter labels causing UI errors. ([#1425](https://github.com/stashapp/stash/pull/1425)) * Fix post-processing not running when scraping by performer fragment. ([#1387](https://github.com/stashapp/stash/pull/1387)) diff --git a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx index b2d4fdf38..aaa228187 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/Movie.tsx @@ -15,8 +15,6 @@ import { Modal, } from "src/components/Shared"; import { useToast } from "src/hooks"; -import { Modal as BSModal, Button } from "react-bootstrap"; -import { ImageUtils } from "src/utils"; import { MovieScenesPanel } from "./MovieScenesPanel"; import { MovieDetailsPanel } from "./MovieDetailsPanel"; import { MovieEditPanel } from "./MovieEditPanel"; @@ -34,7 +32,6 @@ export const Movie: React.FC = () => { // Editing state const [isEditing, setIsEditing] = useState(isNew); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); - const [isImageAlertOpen, setIsImageAlertOpen] = useState(false); // Editing movie state const [frontImage, setFrontImage] = useState( @@ -43,11 +40,7 @@ export const Movie: React.FC = () => { const [backImage, setBackImage] = useState( undefined ); - - // Movie state - const [imageClipboard, setImageClipboard] = useState( - undefined - ); + const [encodingImage, setEncodingImage] = useState(false); // Network state const { data, error, loading } = useFindMovie(id); @@ -69,23 +62,7 @@ export const Movie: React.FC = () => { }; }); - function showImageAlert(imageData: string) { - setImageClipboard(imageData); - setIsImageAlertOpen(true); - } - - function setImageFromClipboard(isFrontImage: boolean) { - if (isFrontImage) { - setFrontImage(imageClipboard); - } else { - setBackImage(imageClipboard); - } - - setImageClipboard(undefined); - setIsImageAlertOpen(false); - } - - const encodingImage = ImageUtils.usePasteImage(showImageAlert, isEditing); + const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding); if (!isNew && !isEditing) { if (!data || !data.findMovie || loading) return ; @@ -99,8 +76,6 @@ export const Movie: React.FC = () => { ) { const ret: Partial = { ...input, - front_image: frontImage, - back_image: backImage, }; if (!isNew) { @@ -174,43 +149,6 @@ export const Movie: React.FC = () => { ); } - function renderImageAlert() { - return ( - setIsImageAlertOpen(false)} - > - -

Select image to set

-
- -
- - - - -
-
-
- ); - } - function renderFrontImage() { let image = movie?.front_image_path; if (isEditing) { @@ -292,6 +230,7 @@ export const Movie: React.FC = () => { onDelete={onDelete} setFrontImage={setFrontImage} setBackImage={setBackImage} + onImageEncoding={onImageEncoding} /> )} @@ -302,7 +241,6 @@ export const Movie: React.FC = () => { )} {renderDeleteAlert()} - {renderImageAlert()} ); }; diff --git a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx index 4c9a71aa0..5247c4baa 100644 --- a/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx +++ b/ui/v2.5/src/components/Movies/MovieDetails/MovieEditPanel.tsx @@ -14,7 +14,14 @@ import { DurationInput, } from "src/components/Shared"; import { useToast } from "src/hooks"; -import { Form, Button, Col, Row, InputGroup } from "react-bootstrap"; +import { + Modal as BSModal, + 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"; @@ -30,6 +37,7 @@ interface IMovieEditPanel { onDelete: () => void; setFrontImage: (image?: string | null) => void; setBackImage: (image?: string | null) => void; + onImageEncoding: (loading?: boolean) => void; } export const MovieEditPanel: React.FC = ({ @@ -39,12 +47,18 @@ export const MovieEditPanel: React.FC = ({ onDelete, setFrontImage, setBackImage, + onImageEncoding, }) => { const Toast = useToast(); const isNew = movie === undefined; const [isLoading, setIsLoading] = useState(false); + const [isImageAlertOpen, setIsImageAlertOpen] = useState(false); + + const [imageClipboard, setImageClipboard] = useState( + undefined + ); const Scrapers = useListMovieScrapers(); const [scrapedMovie, setScrapedMovie] = useState< @@ -70,6 +84,8 @@ export const MovieEditPanel: React.FC = ({ director: yup.string().optional().nullable(), synopsis: yup.string().optional().nullable(), url: yup.string().optional().nullable(), + front_image: yup.string().optional().nullable(), + back_image: yup.string().optional().nullable(), }); const initialValues = { @@ -77,11 +93,13 @@ export const MovieEditPanel: React.FC = ({ aliases: movie?.aliases, duration: movie?.duration, date: movie?.date, - rating: movie?.rating, + rating: movie?.rating ?? null, studio_id: movie?.studio?.id, director: movie?.director, synopsis: movie?.synopsis, url: movie?.url, + front_image: undefined, + back_image: undefined, }; type InputValues = typeof initialValues; @@ -92,6 +110,21 @@ export const MovieEditPanel: React.FC = ({ onSubmit: (values) => onSubmit(getMovieInput(values)), }); + const encodingImage = ImageUtils.usePasteImage(showImageAlert); + + useEffect(() => { + setFrontImage(formik.values.front_image); + }, [formik.values.front_image, setFrontImage]); + + useEffect(() => { + setBackImage(formik.values.back_image); + }, [formik.values.back_image, setBackImage]); + + useEffect(() => onImageEncoding(encodingImage), [ + onImageEncoding, + encodingImage, + ]); + function setRating(v: number) { formik.setFieldValue("rating", v); } @@ -122,6 +155,22 @@ export const MovieEditPanel: React.FC = ({ }; }); + function showImageAlert(imageData: string) { + setImageClipboard(imageData); + setIsImageAlertOpen(true); + } + + function setImageFromClipboard(isFrontImage: boolean) { + if (isFrontImage) { + formik.setFieldValue("front_image", imageClipboard); + } else { + formik.setFieldValue("back_image", imageClipboard); + } + + setImageClipboard(undefined); + setIsImageAlertOpen(false); + } + function getMovieInput(values: InputValues) { const input: Partial = { ...values, @@ -172,10 +221,10 @@ export const MovieEditPanel: React.FC = ({ } const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image; - setFrontImage(imageStr ?? undefined); + formik.setFieldValue("front_image", imageStr ?? undefined); const backImageStr = (state as GQL.ScrapedMovieDataFragment).back_image; - setBackImage(backImageStr ?? undefined); + formik.setFieldValue("back_image", backImageStr ?? undefined); } async function onScrapeMovieURL() { @@ -256,11 +305,52 @@ export const MovieEditPanel: React.FC = ({ } function onFrontImageChange(event: React.FormEvent) { - ImageUtils.onImageChange(event, setFrontImage); + ImageUtils.onImageChange(event, (data) => + formik.setFieldValue("front_image", data) + ); } function onBackImageChange(event: React.FormEvent) { - ImageUtils.onImageChange(event, setBackImage); + ImageUtils.onImageChange(event, (data) => + formik.setFieldValue("back_image", data) + ); + } + + function renderImageAlert() { + return ( + setIsImageAlertOpen(false)} + > + +

Select image to set

+
+ +
+ + + + +
+
+
+ ); } if (isLoading) return ; @@ -357,7 +447,9 @@ export const MovieEditPanel: React.FC = ({ formik.setFieldValue("rating", value)} + onSetRating={(value) => + formik.setFieldValue("rating", value ?? null) + } /> @@ -399,20 +491,22 @@ export const MovieEditPanel: React.FC = ({ isEditing={isEditing} onToggleEdit={onCancel} onSave={() => formik.handleSubmit()} + saveDisabled={!formik.dirty} onImageChange={onFrontImageChange} - onImageChangeURL={setFrontImage} + onImageChangeURL={(i) => formik.setFieldValue("front_image", i)} onClearImage={() => { - setFrontImage(null); + formik.setFieldValue("front_image", null); }} onBackImageChange={onBackImageChange} - onBackImageChangeURL={setBackImage} + onBackImageChangeURL={(i) => formik.setFieldValue("back_image", i)} onClearBackImage={() => { - setBackImage(null); + formik.setFieldValue("back_image", null); }} onDelete={onDelete} /> {maybeRenderScrapeDialog()} + {renderImageAlert()} ); }; diff --git a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx index a8c345f40..33e8b92be 100644 --- a/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx +++ b/ui/v2.5/src/components/Performers/PerformerDetails/PerformerEditPanel.tsx @@ -144,7 +144,7 @@ export const PerformerEditPanel: React.FC = ({ tag_ids: (performer.tags ?? []).map((t) => t.id), stash_ids: performer.stash_ids ?? undefined, image: undefined, - rating: performer.rating ?? undefined, + rating: performer.rating ?? null, details: performer.details ?? "", death_date: performer.death_date ?? "", hair_color: performer.hair_color ?? "", @@ -691,6 +691,7 @@ export const PerformerEditPanel: React.FC = ({ - +
+
+
+ + +
+ + {maybeRenderStashboxQueryButton()} + {renderScraperMenu()} +
- - {maybeRenderStashboxQueryButton()} - {renderScraperMenu()} - - -
-
- {FormUtils.renderInputGroup({ - title: "Title", - value: title, - onChange: setTitle, - isEditing: true, - })} - - - URL -
- {maybeRenderScrapeButton()} -
- - - {EditableTextUtils.renderInputGroup({ - title: "URL", - value: url, - onChange: setUrl, - isEditing: true, - })} - -
- {FormUtils.renderInputGroup({ - title: "Date", - value: date, - isEditing: true, - onChange: setDate, - placeholder: "YYYY-MM-DD", - })} - - {FormUtils.renderLabel({ - title: "Rating", - })} - - setRating(value)} - /> - - - - {FormUtils.renderLabel({ - title: "Galleries", - })} - - setGalleries(items)} - /> - - - - - {FormUtils.renderLabel({ - title: "Studio", - })} - - - setStudioId(items.length > 0 ? items[0]?.id : undefined) - } - ids={studioId ? [studioId] : []} - /> - - - - - {FormUtils.renderLabel({ - title: "Performers", - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - - setPerformerIds(items.map((item) => item.id)) - } - ids={performerIds} - /> - - - - - {FormUtils.renderLabel({ - title: "Movies/Scenes", - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setMovieIds(items.map((item) => item.id))} - ids={movieIds} - /> - {renderTableMovies()} - - - - - {FormUtils.renderLabel({ - title: "Tags", - labelProps: { - column: true, - sm: 3, - xl: 12, - }, - })} - - setTagIds(items.map((item) => item.id))} - ids={tagIds} - /> - - - - StashIDs -
    - {stashIDs.map((stashID) => { - const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; - const link = base ? ( - - {stashID.stash_id} - - ) : ( - stashID.stash_id - ); - return ( -
  • - - {link} -
  • - ); - })} -
-
-
-
- - Details - ) => - setDetails(newValue.currentTarget.value) - } - value={details} - /> - -
- - Cover Image - {imageEncoding ? ( - - ) : ( - Scene cover +
+ {renderTextField("title", "Title")} + + + URL +
+ {maybeRenderScrapeButton()} +
+ + + - )} - + +
+ {renderTextField("date", "Date", "YYYY-MM-DD")} + + {FormUtils.renderLabel({ + title: "Rating", + })} + + + formik.setFieldValue("rating", value ?? null) + } + /> + + + + {FormUtils.renderLabel({ + title: "Galleries", + })} + + setGalleries(items)} + /> + + + + + {FormUtils.renderLabel({ + title: "Studio", + })} + + + formik.setFieldValue( + "studio_id", + items.length > 0 ? items[0]?.id : undefined + ) + } + ids={formik.values.studio_id ? [formik.values.studio_id] : []} + /> + + + + + {FormUtils.renderLabel({ + title: "Performers", + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + + formik.setFieldValue( + "performer_ids", + items.map((item) => item.id) + ) + } + ids={formik.values.performer_ids} + /> + + + + + {FormUtils.renderLabel({ + title: "Movies/Scenes", + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + + setMovieIds(items.map((item) => item.id)) + } + ids={formik.values.movies.map((m) => m.movie_id)} + /> + {renderTableMovies()} + + + + + {FormUtils.renderLabel({ + title: "Tags", + labelProps: { + column: true, + sm: 3, + xl: 12, + }, + })} + + + formik.setFieldValue( + "tag_ids", + items.map((item) => item.id) + ) + } + ids={formik.values.tag_ids} + /> + + + + StashIDs +
    + {formik.values.stash_ids.map((stashID) => { + const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0]; + const link = base ? ( + + {stashID.stash_id} + + ) : ( + stashID.stash_id + ); + return ( +
  • + + {link} +
  • + ); + })} +
+
+ + Details + ) => + formik.setFieldValue("details", newValue.currentTarget.value) + } + value={formik.values.details} + /> + +
+ + Cover Image + {imageEncoding ? ( + + ) : ( + Scene cover + )} + + +
+
-
+
); }; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx index 5b4e62aef..fd04969b2 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/SceneMovieTable.tsx @@ -6,8 +6,8 @@ import { Form, Row, Col } from "react-bootstrap"; export type MovieSceneIndexMap = Map; export interface IProps { - movieSceneIndexes: MovieSceneIndexMap; - onUpdate: (value: MovieSceneIndexMap) => void; + movieScenes: GQL.SceneMovieInput[]; + onUpdate: (value: GQL.SceneMovieInput[]) => void; } export const SceneMovieTable: React.FunctionComponent = ( @@ -16,40 +16,43 @@ export const SceneMovieTable: React.FunctionComponent = ( const { data } = useAllMoviesForFilter(); const items = !!data && !!data.allMovies ? data.allMovies : []; - let itemsFilter: GQL.SlimMovieDataFragment[] = []; - if (!!props.movieSceneIndexes && !!items) { - props.movieSceneIndexes.forEach((_index, movieId) => { - itemsFilter = itemsFilter.concat(items.filter((x) => x.id === movieId)); - }); - } - - const storeIdx = itemsFilter.map((movie) => { - return props.movieSceneIndexes.get(movie.id); + const movieEntries = props.movieScenes.map((m) => { + return { + movie: items.find((mm) => m.movie_id === mm.id), + ...m, + }; }); const updateFieldChanged = (movieId: string, value: number) => { - const newMap = new Map(props.movieSceneIndexes); - newMap.set(movieId, value); - props.onUpdate(newMap); + const newValues = props.movieScenes.map((ms) => { + if (ms.movie_id === movieId) { + return { + movie_id: movieId, + scene_index: value, + }; + } + return ms; + }); + props.onUpdate(newValues); }; function renderTableData() { return ( <> - {itemsFilter!.map((item, index: number) => ( - + {movieEntries.map((m) => ( + - {item.name} + {m.movie?.name ?? ""} ) => { updateFieldChanged( - item.id, + m.movie_id, Number.parseInt( e.currentTarget.value ? e.currentTarget.value : "0", 10 @@ -64,7 +67,7 @@ export const SceneMovieTable: React.FunctionComponent = ( ); } - if (props.movieSceneIndexes.size > 0) { + if (props.movieScenes.length > 0) { return (
diff --git a/ui/v2.5/src/components/Settings/SettingsDLNAPanel.tsx b/ui/v2.5/src/components/Settings/SettingsDLNAPanel.tsx index bafa86a9b..0734e0c97 100644 --- a/ui/v2.5/src/components/Settings/SettingsDLNAPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsDLNAPanel.tsx @@ -480,7 +480,7 @@ export const SettingsDLNAPanel: React.FC = () => {
- diff --git a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx index cfcbd135e..1cf42d8d4 100644 --- a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx +++ b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx @@ -8,6 +8,7 @@ interface IProps { isEditing: boolean; onToggleEdit: () => void; onSave: () => void; + saveDisabled?: boolean; onDelete: () => void; onAutoTag?: () => void; onImageChange: (event: React.FormEvent) => void; @@ -39,7 +40,12 @@ export const DetailsEditNavbar: React.FC = (props: IProps) => { if (!props.isEditing) return; return ( - );