Fix performer image display again and refactoring (#3782)

* Fix the fix for displayed performer image sticking after save
* Refactor for consistency
* Fully extract entity create/update logic from edit pages
* Fix submit hotkeys
* Refactor scene cover preview
* Fix atoi error on new scene page
This commit is contained in:
DingDongSoLong4 2023-05-31 02:39:22 +02:00 committed by GitHub
parent fc53380310
commit d0847d1ebf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
21 changed files with 436 additions and 332 deletions

View file

@ -64,6 +64,23 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
const [organizedLoading, setOrganizedLoading] = useState(false);
async function onSave(input: GQL.GalleryCreateInput) {
await updateGallery({
variables: {
input: {
id: gallery.id,
...input,
},
},
});
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() }
),
});
}
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
@ -242,6 +259,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
<GalleryEditPanel
isVisible={activeTabKey === "gallery-edit-panel"}
gallery={gallery}
onSubmit={onSave}
onDelete={() => setIsDeleteAlertOpen(true)}
/>
</Tab.Pane>

View file

@ -1,16 +1,39 @@
import React, { useMemo } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useLocation } from "react-router-dom";
import { useHistory, useLocation } from "react-router-dom";
import * as GQL from "src/core/generated-graphql";
import { useGalleryCreate } from "src/core/StashService";
import { useToast } from "src/hooks/Toast";
import { GalleryEditPanel } from "./GalleryEditPanel";
const GalleryCreate: React.FC = () => {
const history = useHistory();
const intl = useIntl();
const Toast = useToast();
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const gallery = {
title: query.get("q") ?? undefined,
};
const [createGallery] = useGalleryCreate();
async function onSave(input: GQL.GalleryCreateInput) {
const result = await createGallery({
variables: { input },
});
if (result.data?.galleryCreate) {
history.push(`/galleries/${result.data.galleryCreate.id}`);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{ entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() }
),
});
}
}
return (
<div className="row new-view">
<div className="col-md-6">
@ -20,7 +43,12 @@ const GalleryCreate: React.FC = () => {
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
/>
</h2>
<GalleryEditPanel gallery={gallery} isVisible onDelete={() => {}} />
<GalleryEditPanel
gallery={gallery}
isVisible
onSubmit={onSave}
onDelete={() => {}}
/>
</div>
</div>
);

View file

@ -1,6 +1,6 @@
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useHistory, Prompt } from "react-router-dom";
import { Prompt } from "react-router-dom";
import {
Button,
Dropdown,
@ -15,8 +15,6 @@ import * as yup from "yup";
import {
queryScrapeGallery,
queryScrapeGalleryURL,
useGalleryCreate,
useGalleryUpdate,
useListGalleryScrapers,
mutateReloadScrapers,
} from "src/core/StashService";
@ -44,17 +42,18 @@ import { DateInput } from "src/components/Shared/DateInput";
interface IProps {
gallery: Partial<GQL.GalleryDataFragment>;
isVisible: boolean;
onSubmit: (input: GQL.GalleryCreateInput) => Promise<void>;
onDelete: () => void;
}
export const GalleryEditPanel: React.FC<IProps> = ({
gallery,
isVisible,
onSubmit,
onDelete,
}) => {
const intl = useIntl();
const Toast = useToast();
const history = useHistory();
const [scenes, setScenes] = useState<{ id: string; title: string }[]>(
(gallery?.scenes ?? []).map((s) => ({
id: s.id,
@ -74,9 +73,6 @@ export const GalleryEditPanel: React.FC<IProps> = ({
// Network state
const [isLoading, setIsLoading] = useState(false);
const [createGallery] = useGalleryCreate();
const [updateGallery] = useGalleryUpdate();
const titleRequired =
isNew || (gallery?.files?.length === 0 && !gallery?.folder);
@ -151,7 +147,9 @@ export const GalleryEditPanel: React.FC<IProps> = ({
useEffect(() => {
if (isVisible) {
Mousetrap.bind("s s", () => {
formik.handleSubmit();
if (formik.dirty) {
formik.submitForm();
}
});
Mousetrap.bind("d d", () => {
onDelete();
@ -174,51 +172,11 @@ export const GalleryEditPanel: React.FC<IProps> = ({
setQueryableScrapers(newQueryableScrapers);
}, [Scrapers]);
async function onSave(input: GQL.GalleryCreateInput) {
async function onSave(input: InputValues) {
setIsLoading(true);
try {
if (isNew) {
const result = await createGallery({
variables: {
input,
},
});
if (result.data?.galleryCreate) {
history.push(`/galleries/${result.data.galleryCreate.id}`);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{
entity: intl
.formatMessage({ id: "gallery" })
.toLocaleLowerCase(),
}
),
});
}
} else {
const result = await updateGallery({
variables: {
input: {
id: gallery.id!,
...input,
},
},
});
if (result.data?.galleryUpdate) {
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{
entity: intl
.formatMessage({ id: "gallery" })
.toLocaleLowerCase(),
}
),
});
formik.resetForm();
}
}
await onSubmit(input);
formik.resetForm();
} catch (e) {
Toast.error(e);
}

View file

@ -17,6 +17,7 @@ import { Icon } from "src/components/Shared/Icon";
import { Counter } from "src/components/Shared/Counter";
import { useToast } from "src/hooks/Toast";
import * as Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
import { ImageFileInfoPanel } from "./ImageFileInfoPanel";
@ -51,6 +52,18 @@ export const Image: React.FC = () => {
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
async function onSave(input: GQL.ImageUpdateInput) {
await updateImage({
variables: { input },
});
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
),
});
}
async function onRescan() {
if (!image || !image.visual_files.length) {
return;
@ -225,6 +238,7 @@ export const Image: React.FC = () => {
<ImageEditPanel
isVisible={activeTabKey === "image-edit-panel"}
image={image}
onSubmit={onSave}
onDelete={() => setIsDeleteAlertOpen(true)}
/>
</Tab.Pane>

View file

@ -4,7 +4,6 @@ import { FormattedMessage, useIntl } from "react-intl";
import Mousetrap from "mousetrap";
import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
import { useImageUpdate } from "src/core/StashService";
import {
PerformerSelect,
TagSelect,
@ -25,12 +24,14 @@ import { DateInput } from "src/components/Shared/DateInput";
interface IProps {
image: GQL.ImageDataFragment;
isVisible: boolean;
onSubmit: (input: GQL.ImageUpdateInput) => Promise<void>;
onDelete: () => void;
}
export const ImageEditPanel: React.FC<IProps> = ({
image,
isVisible,
onSubmit,
onDelete,
}) => {
const intl = useIntl();
@ -41,8 +42,6 @@ export const ImageEditPanel: React.FC<IProps> = ({
const { configuration } = React.useContext(ConfigurationContext);
const [updateImage] = useImageUpdate();
const schema = yup.object({
title: yup.string().ensure(),
url: yup.string().ensure(),
@ -97,7 +96,9 @@ export const ImageEditPanel: React.FC<IProps> = ({
useEffect(() => {
if (isVisible) {
Mousetrap.bind("s s", () => {
formik.handleSubmit();
if (formik.dirty) {
formik.submitForm();
}
});
Mousetrap.bind("d d", () => {
onDelete();
@ -113,23 +114,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
async function onSave(input: InputValues) {
setIsLoading(true);
try {
const result = await updateImage({
variables: {
input: {
id: image.id,
...input,
},
},
await onSubmit({
id: image.id,
...input,
});
if (result.data?.imageUpdate) {
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
),
});
formik.resetForm();
}
formik.resetForm();
} catch (e) {
Toast.error(e);
}

View file

@ -83,7 +83,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
// set up hotkeys
useEffect(() => {
Mousetrap.bind("e", () => setIsEditing(true));
Mousetrap.bind("e", () => toggleEditing());
Mousetrap.bind("d d", () => {
onDelete();
});
@ -95,22 +95,21 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
});
async function onSave(input: GQL.MovieCreateInput) {
try {
const result = await updateMovie({
variables: {
input: {
id: movie.id,
...input,
},
await updateMovie({
variables: {
input: {
id: movie.id,
...input,
},
});
if (result.data?.movieUpdate) {
setIsEditing(false);
history.push(`/movies/${result.data.movieUpdate.id}`);
}
} catch (e) {
Toast.error(e);
}
},
});
toggleEditing(false);
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "movie" }).toLocaleLowerCase() }
),
});
}
async function onDelete() {
@ -124,8 +123,12 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
history.push(`/movies`);
}
function onToggleEdit() {
setIsEditing(!isEditing);
function toggleEditing(value?: boolean) {
if (value !== undefined) {
setIsEditing(value);
} else {
setIsEditing((e) => !e);
}
setFrontImage(undefined);
setBackImage(undefined);
}
@ -239,7 +242,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
objectName={movie.name}
isNew={false}
isEditing={isEditing}
onToggleEdit={onToggleEdit}
onToggleEdit={() => toggleEditing()}
onSave={() => {}}
onImageChange={() => {}}
onDelete={onDelete}
@ -249,7 +252,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
<MovieEditPanel
movie={movie}
onSubmit={onSave}
onCancel={onToggleEdit}
onCancel={() => toggleEditing()}
onDelete={onDelete}
setFrontImage={setFrontImage}
setBackImage={setBackImage}

View file

@ -2,15 +2,17 @@ import React, { useMemo, useState } from "react";
import * as GQL from "src/core/generated-graphql";
import { useMovieCreate } from "src/core/StashService";
import { useHistory, useLocation } from "react-router-dom";
import { useIntl } from "react-intl";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useToast } from "src/hooks/Toast";
import { MovieEditPanel } from "./MovieEditPanel";
const MovieCreate: React.FC = () => {
const history = useHistory();
const location = useLocation();
const intl = useIntl();
const Toast = useToast();
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const movie = {
name: query.get("q") ?? undefined,
@ -24,15 +26,17 @@ const MovieCreate: React.FC = () => {
const [createMovie] = useMovieCreate();
async function onSave(input: GQL.MovieCreateInput) {
try {
const result = await createMovie({
variables: input,
const result = await createMovie({
variables: input,
});
if (result.data?.movieCreate?.id) {
history.push(`/movies/${result.data.movieCreate.id}`);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{ entity: intl.formatMessage({ id: "gallery" }).toLocaleLowerCase() }
),
});
if (result.data?.movieCreate?.id) {
history.push(`/movies/${result.data.movieCreate.id}`);
}
} catch (e) {
Toast.error(e);
}
}

View file

@ -28,7 +28,7 @@ import { DateInput } from "src/components/Shared/DateInput";
interface IMovieEditPanel {
movie: Partial<GQL.MovieDataFragment>;
onSubmit: (movie: GQL.MovieCreateInput) => void;
onSubmit: (movie: GQL.MovieCreateInput) => Promise<void>;
onCancel: () => void;
onDelete: () => void;
setFrontImage: (image?: string | null) => void;
@ -103,7 +103,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
initialValues,
enableReinitialize: true,
validationSchema: schema,
onSubmit: (values) => onSubmit(values),
onSubmit: (values) => onSave(values),
});
function setRating(v: number) {
@ -116,19 +116,17 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
setRating
);
function onCancelEditing() {
setFrontImage(undefined);
setBackImage(undefined);
onCancel?.();
}
// set up hotkeys
useEffect(() => {
// Mousetrap.bind("u", (e) => {
// setStudioFocus()
// e.preventDefault();
// });
Mousetrap.bind("s s", () => formik.handleSubmit());
Mousetrap.bind("s s", () => {
if (formik.dirty) {
formik.submitForm();
}
});
return () => {
// Mousetrap.unbind("u");
@ -182,6 +180,17 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
}
}
async function onSave(input: InputValues) {
setIsLoading(true);
try {
await onSubmit(input);
formik.resetForm();
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
}
async function onScrapeMovieURL() {
const { url } = formik.values;
if (!url) return;
@ -488,7 +497,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
isNew={isNew}
isEditing={isEditing}
onToggleEdit={onCancelEditing}
onToggleEdit={onCancel}
onSave={formik.handleSubmit}
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
onImageChange={onFrontImageChange}

View file

@ -68,15 +68,17 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
const activeImage = useMemo(() => {
const performerImage = performer.image_path;
if (image === null && performerImage) {
const performerImageURL = new URL(performerImage);
performerImageURL.searchParams.set("default", "true");
return performerImageURL.toString();
} else if (image) {
return image;
if (isEditing) {
if (image === null && performerImage) {
const performerImageURL = new URL(performerImage);
performerImageURL.searchParams.set("default", "true");
return performerImageURL.toString();
} else if (image) {
return image;
}
}
return performerImage;
}, [image, performer.image_path]);
}, [image, isEditing, performer.image_path]);
const lightboxImages = useMemo(
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
@ -122,15 +124,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
setRating
);
// reset image if performer changed
useEffect(() => {
setImage(undefined);
}, [performer]);
// set up hotkeys
useEffect(() => {
Mousetrap.bind("a", () => setActiveTabKey("details"));
Mousetrap.bind("e", () => setIsEditing(!isEditing));
Mousetrap.bind("e", () => toggleEditing());
Mousetrap.bind("c", () => setActiveTabKey("scenes"));
Mousetrap.bind("g", () => setActiveTabKey("galleries"));
Mousetrap.bind("m", () => setActiveTabKey("movies"));
@ -147,6 +144,24 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
};
});
async function onSave(input: GQL.PerformerCreateInput) {
await updatePerformer({
variables: {
input: {
id: performer.id,
...input,
},
},
});
toggleEditing(false);
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "performer" }).toLocaleLowerCase() }
),
});
}
async function onDelete() {
try {
await deletePerformer({ variables: { id: performer.id } });
@ -158,6 +173,15 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
history.push("/performers");
}
function toggleEditing(value?: boolean) {
if (value !== undefined) {
setIsEditing(value);
} else {
setIsEditing((e) => !e);
}
setImage(undefined);
}
function renderImage() {
if (activeImage) {
return (
@ -175,9 +199,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
objectName={
performer?.name ?? intl.formatMessage({ id: "performer" })
}
onToggleEdit={() => {
setIsEditing(!isEditing);
}}
onToggleEdit={() => toggleEditing()}
onDelete={onDelete}
onAutoTag={onAutoTag}
isNew={false}
@ -297,7 +319,8 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
<PerformerEditPanel
performer={performer}
isVisible={isEditing}
onCancel={() => setIsEditing(false)}
onSubmit={onSave}
onCancel={() => toggleEditing()}
setImage={setImage}
setEncodingImage={setEncodingImage}
/>

View file

@ -2,9 +2,16 @@ import React, { useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { PerformerEditPanel } from "./PerformerEditPanel";
import { useLocation } from "react-router-dom";
import { useHistory, useLocation } from "react-router-dom";
import { useToast } from "src/hooks/Toast";
import * as GQL from "src/core/generated-graphql";
import { usePerformerCreate } from "src/core/StashService";
const PerformerCreate: React.FC = () => {
const Toast = useToast();
const history = useHistory();
const intl = useIntl();
const [image, setImage] = useState<string | null>();
const [encodingImage, setEncodingImage] = useState<boolean>(false);
@ -14,7 +21,24 @@ const PerformerCreate: React.FC = () => {
name: query.get("q") ?? undefined,
};
const intl = useIntl();
const [createPerformer] = usePerformerCreate();
async function onSave(input: GQL.PerformerCreateInput) {
const result = await createPerformer({
variables: { input },
});
if (result.data?.performerCreate) {
history.push(`/performers/${result.data.performerCreate.id}`);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{
entity: intl.formatMessage({ id: "performer" }).toLocaleLowerCase(),
}
),
});
}
}
function renderPerformerImage() {
if (encodingImage) {
@ -46,6 +70,7 @@ const PerformerCreate: React.FC = () => {
<PerformerEditPanel
performer={performer}
isVisible
onSubmit={onSave}
setImage={setImage}
setEncodingImage={setEncodingImage}
/>

View file

@ -8,8 +8,6 @@ import {
useListPerformerScrapers,
queryScrapePerformer,
mutateReloadScrapers,
usePerformerUpdate,
usePerformerCreate,
useTagCreate,
queryScrapePerformerURL,
} from "src/core/StashService";
@ -24,7 +22,7 @@ import ImageUtils from "src/utils/image";
import { getStashIDs } from "src/utils/stashIds";
import { stashboxDisplayName } from "src/utils/stashbox";
import { useToast } from "src/hooks/Toast";
import { Prompt, useHistory } from "react-router-dom";
import { Prompt } from "react-router-dom";
import { useFormik } from "formik";
import {
genderToString,
@ -57,6 +55,7 @@ const isScraper = (
interface IPerformerDetails {
performer: Partial<GQL.PerformerDataFragment>;
isVisible: boolean;
onSubmit: (performer: GQL.PerformerCreateInput) => Promise<void>;
onCancel?: () => void;
setImage: (image?: string | null) => void;
setEncodingImage: (loading: boolean) => void;
@ -65,12 +64,12 @@ interface IPerformerDetails {
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
performer,
isVisible,
onSubmit,
onCancel,
setImage,
setEncodingImage,
}) => {
const Toast = useToast();
const history = useHistory();
const isNew = performer.id === undefined;
@ -82,9 +81,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
// Network state
const [isLoading, setIsLoading] = useState(false);
const [updatePerformer] = usePerformerUpdate();
const [createPerformer] = usePerformerCreate();
const Scrapers = useListPerformerScrapers();
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
@ -454,61 +450,35 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
ImageUtils.onImageChange(event, onImageLoad);
}
function valuesToInput(input: InputValues): GQL.PerformerCreateInput {
return {
...input,
gender: input.gender || null,
height_cm: input.height_cm || null,
weight: input.weight || null,
penis_length: input.penis_length || null,
circumcised: input.circumcised || null,
};
}
async function onSave(input: InputValues) {
setIsLoading(true);
try {
if (isNew) {
const result = await createPerformer({
variables: {
input: {
...input,
gender: input.gender || null,
height_cm: input.height_cm || null,
weight: input.weight || null,
penis_length: input.penis_length || null,
circumcised: input.circumcised || null,
},
},
});
if (result.data?.performerCreate) {
history.push(`/performers/${result.data.performerCreate.id}`);
}
} else {
await updatePerformer({
variables: {
input: {
id: performer.id!,
...input,
gender: input.gender || null,
height_cm: input.height_cm || null,
weight: input.weight || null,
penis_length: input.penis_length || null,
circumcised: input.circumcised || null,
},
},
});
}
await onSubmit(valuesToInput(input));
formik.resetForm();
} catch (e) {
Toast.error(e);
setIsLoading(false);
return;
}
if (!isNew && onCancel) {
onCancel();
}
setIsLoading(false);
}
function onCancelEditing() {
setImage(undefined);
onCancel?.();
}
// set up hotkeys
useEffect(() => {
if (isVisible) {
Mousetrap.bind("s s", () => {
onSave?.(formik.values);
if (formik.dirty) {
formik.submitForm();
}
});
return () => {
@ -699,9 +669,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
}
const currentPerformer = {
...formik.values,
gender: formik.values.gender || null,
circumcised: formik.values.circumcised || null,
...valuesToInput(formik.values),
image: formik.values.image ?? performer.image_path,
};
@ -729,7 +697,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
return (
<div className={cx("details-edit", "col-xl-9", classNames)}>
{!isNew && onCancel ? (
<Button className="mr-2" variant="primary" onClick={onCancelEditing}>
<Button className="mr-2" variant="primary" onClick={onCancel}>
<FormattedMessage id="actions.cancel" />
</Button>
) : null}

View file

@ -169,6 +169,23 @@ const ScenePage: React.FC<IProps> = ({
};
});
async function onSave(input: GQL.SceneCreateInput) {
await updateScene({
variables: {
input: {
id: scene.id,
...input,
},
},
});
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase() }
),
});
}
const onOrganizedClick = async () => {
try {
setOrganizedLoading(true);
@ -461,6 +478,7 @@ const ScenePage: React.FC<IProps> = ({
<SceneEditPanel
isVisible={activeTabKey === "scene-edit-panel"}
scene={scene}
onSubmit={onSave}
onDelete={() => setIsDeleteAlertOpen(true)}
/>
</Tab.Pane>

View file

@ -1,19 +1,23 @@
import React, { useEffect, useMemo, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useLocation } from "react-router-dom";
import { useHistory, useLocation } from "react-router-dom";
import { SceneEditPanel } from "./SceneEditPanel";
import { useFindScene } from "src/core/StashService";
import * as GQL from "src/core/generated-graphql";
import { mutateCreateScene, useFindScene } from "src/core/StashService";
import ImageUtils from "src/utils/image";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { useToast } from "src/hooks/Toast";
const SceneCreate: React.FC = () => {
const history = useHistory();
const intl = useIntl();
const Toast = useToast();
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
// create scene from provided scene id if applicable
const { data, loading } = useFindScene(query.get("from_scene_id") ?? "");
const { data, loading } = useFindScene(query.get("from_scene_id") ?? "new");
const [loadingCoverImage, setLoadingCoverImage] = useState(false);
const [coverImage, setCoverImage] = useState<string>();
@ -53,6 +57,23 @@ const SceneCreate: React.FC = () => {
return <LoadingIndicator />;
}
async function onSave(input: GQL.SceneCreateInput) {
const fileID = query.get("file_id") ?? undefined;
const result = await mutateCreateScene({
...input,
file_ids: fileID ? [fileID] : undefined,
});
if (result.data?.sceneCreate?.id) {
history.push(`/scenes/${result.data.sceneCreate.id}`);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{ entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase() }
),
});
}
}
return (
<div className="row new-view justify-content-center" id="create-scene-page">
<div className="col-md-8">
@ -64,10 +85,10 @@ const SceneCreate: React.FC = () => {
</h2>
<SceneEditPanel
scene={scene}
fileID={query.get("file_id") ?? undefined}
initialCoverImage={coverImage}
isVisible
isNew
onSubmit={onSave}
/>
</div>
</div>

View file

@ -16,10 +16,8 @@ import {
queryScrapeScene,
queryScrapeSceneURL,
useListSceneScrapers,
useSceneUpdate,
mutateReloadScrapers,
queryScrapeSceneQueryFragment,
mutateCreateScene,
} from "src/core/StashService";
import {
PerformerSelect,
@ -37,7 +35,7 @@ import ImageUtils from "src/utils/image";
import FormUtils from "src/utils/form";
import { getStashIDs } from "src/utils/stashIds";
import { useFormik } from "formik";
import { Prompt, useHistory } from "react-router-dom";
import { Prompt } from "react-router-dom";
import { ConfigurationContext } from "src/hooks/Config";
import { stashboxDisplayName } from "src/utils/stashbox";
import { SceneMovieTable } from "./SceneMovieTable";
@ -59,24 +57,23 @@ const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
interface IProps {
scene: Partial<GQL.SceneDataFragment>;
fileID?: string;
initialCoverImage?: string;
isNew?: boolean;
isVisible: boolean;
onSubmit: (input: GQL.SceneCreateInput) => Promise<void>;
onDelete?: () => void;
}
export const SceneEditPanel: React.FC<IProps> = ({
scene,
fileID,
initialCoverImage,
isNew = false,
isVisible,
onSubmit,
onDelete,
}) => {
const intl = useIntl();
const Toast = useToast();
const history = useHistory();
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
[]
@ -92,14 +89,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
const [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
const [endpoint, setEndpoint] = useState<string>();
const [coverImagePreview, setCoverImagePreview] = useState<string>();
useEffect(() => {
setCoverImagePreview(
initialCoverImage ?? scene.paths?.screenshot ?? undefined
);
}, [scene.paths?.screenshot, initialCoverImage]);
useEffect(() => {
setGalleries(
scene.galleries?.map((g) => ({
@ -114,8 +103,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
// Network state
const [isLoading, setIsLoading] = useState(false);
const [updateScene] = useSceneUpdate();
const schema = yup.object({
title: yup.string().ensure(),
code: yup.string().ensure(),
@ -183,6 +170,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
onSubmit: (values) => onSave(values),
});
const coverImagePreview = useMemo(() => {
const sceneImage = scene.paths?.screenshot;
const formImage = formik.values.cover_image;
if (formImage === null && sceneImage) {
const sceneImageURL = new URL(sceneImage);
sceneImageURL.searchParams.set("default", "true");
return sceneImageURL.toString();
} else if (formImage) {
return formImage;
}
return sceneImage;
}, [formik.values.cover_image, scene.paths?.screenshot]);
function setRating(v: number) {
formik.setFieldValue("rating100", v);
}
@ -209,7 +209,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
useEffect(() => {
if (isVisible) {
Mousetrap.bind("s s", () => {
formik.handleSubmit();
if (formik.dirty) {
formik.submitForm();
}
});
Mousetrap.bind("d d", () => {
if (onDelete) {
@ -259,35 +261,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
async function onSave(input: InputValues) {
setIsLoading(true);
try {
if (!isNew) {
const result = await updateScene({
variables: {
input: {
id: scene.id!,
...input,
},
},
});
if (result.data?.sceneUpdate) {
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{
entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase(),
}
),
});
formik.resetForm();
}
} else {
const result = await mutateCreateScene({
...input,
file_ids: fileID ? [fileID] : undefined,
});
if (result.data?.sceneCreate?.id) {
history.push(`/scenes/${result.data?.sceneCreate.id}`);
}
}
await onSubmit(input);
formik.resetForm();
} catch (e) {
Toast.error(e);
}
@ -318,7 +293,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
function onImageLoad(imageData: string) {
setCoverImagePreview(imageData);
formik.setFieldValue("cover_image", imageData);
}
@ -619,7 +593,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
if (updatedScene.image) {
// image is a base64 string
formik.setFieldValue("cover_image", updatedScene.image);
setCoverImagePreview(updatedScene.image);
}
if (updatedScene.remote_site_id && endpoint) {

View file

@ -69,7 +69,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
// set up hotkeys
useEffect(() => {
Mousetrap.bind("e", () => setIsEditing(true));
Mousetrap.bind("e", () => toggleEditing());
Mousetrap.bind("d d", () => {
onDelete();
});
@ -83,21 +83,21 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
});
async function onSave(input: GQL.StudioCreateInput) {
try {
const result = await updateStudio({
variables: {
input: {
id: studio.id,
...input,
},
await updateStudio({
variables: {
input: {
id: studio.id,
...input,
},
});
if (result.data?.studioUpdate) {
setIsEditing(false);
}
} catch (e) {
Toast.error(e);
}
},
});
toggleEditing(false);
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() }
),
});
}
async function onAutoTag() {
@ -149,8 +149,13 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
);
}
function onToggleEdit() {
setIsEditing(!isEditing);
function toggleEditing(value?: boolean) {
if (value !== undefined) {
setIsEditing(value);
} else {
setIsEditing((e) => !e);
}
setImage(undefined);
}
function renderImage() {
@ -213,7 +218,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
isNew={false}
isEditing={isEditing}
onToggleEdit={onToggleEdit}
onToggleEdit={() => toggleEditing()}
onSave={() => {}}
onImageChange={() => {}}
onClearImage={() => {}}
@ -225,7 +230,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
<StudioEditPanel
studio={studio}
onSubmit={onSave}
onCancel={onToggleEdit}
onCancel={() => toggleEditing()}
onDelete={onDelete}
setImage={setImage}
setEncodingImage={setEncodingImage}

View file

@ -27,15 +27,17 @@ const StudioCreate: React.FC = () => {
const [createStudio] = useStudioCreate();
async function onSave(input: GQL.StudioCreateInput) {
try {
const result = await createStudio({
variables: { input },
const result = await createStudio({
variables: { input },
});
if (result.data?.studioCreate?.id) {
history.push(`/studios/${result.data.studioCreate.id}`);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{ entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() }
),
});
if (result.data?.studioCreate?.id) {
history.push(`/studios/${result.data.studioCreate.id}`);
}
} catch (e) {
Toast.error(e);
}
}

View file

@ -1,9 +1,10 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
import Mousetrap from "mousetrap";
import { Icon } from "src/components/Shared/Icon";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { StudioSelect } from "src/components/Shared/Select";
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
import { Button, Form, Col, Row } from "react-bootstrap";
@ -18,10 +19,11 @@ import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
import { useRatingKeybinds } from "src/hooks/keybinds";
import { ConfigurationContext } from "src/hooks/Config";
import isEqual from "lodash-es/isEqual";
import { useToast } from "src/hooks/Toast";
interface IStudioEditPanel {
studio: Partial<GQL.StudioDataFragment>;
onSubmit: (studio: GQL.StudioCreateInput) => void;
onSubmit: (studio: GQL.StudioCreateInput) => Promise<void>;
onCancel: () => void;
onDelete: () => void;
setImage: (image?: string | null) => void;
@ -37,10 +39,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
setEncodingImage,
}) => {
const intl = useIntl();
const Toast = useToast();
const isNew = studio.id === undefined;
const { configuration } = React.useContext(ConfigurationContext);
// Network state
const [isLoading, setIsLoading] = useState(false);
const schema = yup.object({
name: yup.string().required(),
url: yup.string().ensure(),
@ -73,6 +79,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
});
const initialValues = {
id: studio.id,
name: studio.name ?? "",
url: studio.url ?? "",
details: studio.details ?? "",
@ -89,7 +96,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
initialValues,
enableReinitialize: true,
validationSchema: schema,
onSubmit: (values) => onSubmit(values),
onSubmit: (values) => onSave(values),
});
const encodingImage = ImageUtils.usePasteImage((imageData) =>
@ -114,20 +121,30 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
setRating
);
function onCancelEditing() {
setImage(undefined);
onCancel?.();
}
// set up hotkeys
useEffect(() => {
Mousetrap.bind("s s", () => formik.handleSubmit());
Mousetrap.bind("s s", () => {
if (formik.dirty) {
formik.submitForm();
}
});
return () => {
Mousetrap.unbind("s s");
};
});
async function onSave(input: InputValues) {
setIsLoading(true);
try {
await onSubmit(input);
formik.resetForm();
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
}
function onImageLoad(imageData: string | null) {
formik.setFieldValue("image", imageData);
}
@ -200,6 +217,8 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
: undefined;
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
if (isLoading) return <LoadingIndicator />;
return (
<>
<Prompt
@ -331,7 +350,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
isNew={isNew}
isEditing
onToggleEdit={onCancelEditing}
onToggleEdit={onCancel}
onSave={formik.handleSubmit}
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
onImageChange={onImageChange}

View file

@ -88,7 +88,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
// set up hotkeys
useEffect(() => {
Mousetrap.bind("e", () => setIsEditing(true));
Mousetrap.bind("e", () => toggleEditing());
Mousetrap.bind("d d", () => {
onDelete();
});
@ -106,30 +106,31 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
});
async function onSave(input: GQL.TagCreateInput) {
try {
const oldRelations = {
parents: tag.parents ?? [],
children: tag.children ?? [],
};
const result = await updateTag({
variables: {
input: {
id: tag.id,
...input,
},
const oldRelations = {
parents: tag.parents ?? [],
children: tag.children ?? [],
};
const result = await updateTag({
variables: {
input: {
id: tag.id,
...input,
},
},
});
if (result.data?.tagUpdate) {
toggleEditing(false);
const updated = result.data.tagUpdate;
tagRelationHook(updated, oldRelations, {
parents: updated.parents,
children: updated.children,
});
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase() }
),
});
if (result.data?.tagUpdate) {
setIsEditing(false);
const updated = result.data.tagUpdate;
tagRelationHook(updated, oldRelations, {
parents: updated.parents,
children: updated.children,
});
return updated.id;
}
} catch (e) {
Toast.error(e);
}
}
@ -190,8 +191,12 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
);
}
function onToggleEdit() {
setIsEditing(!isEditing);
function toggleEditing(value?: boolean) {
if (value !== undefined) {
setIsEditing(value);
} else {
setIsEditing((e) => !e);
}
setImage(undefined);
}
@ -283,7 +288,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
objectName={tag.name}
isNew={false}
isEditing={isEditing}
onToggleEdit={onToggleEdit}
onToggleEdit={() => toggleEditing()}
onSave={() => {}}
onImageChange={() => {}}
onClearImage={() => {}}
@ -297,7 +302,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
<TagEditPanel
tag={tag}
onSubmit={onSave}
onCancel={onToggleEdit}
onCancel={() => toggleEditing()}
onDelete={onDelete}
setImage={setImage}
setEncodingImage={setEncodingImage}

View file

@ -1,6 +1,6 @@
import React, { useMemo, useState } from "react";
import { useHistory, useLocation } from "react-router-dom";
import { useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import { useTagCreate } from "src/core/StashService";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
@ -9,10 +9,11 @@ import { tagRelationHook } from "src/core/tags";
import { TagEditPanel } from "./TagEditPanel";
const TagCreate: React.FC = () => {
const intl = useIntl();
const history = useHistory();
const location = useLocation();
const Toast = useToast();
const location = useLocation();
const query = useMemo(() => new URLSearchParams(location.search), [location]);
const tag = {
name: query.get("q") ?? undefined,
@ -25,24 +26,26 @@ const TagCreate: React.FC = () => {
const [createTag] = useTagCreate();
async function onSave(input: GQL.TagCreateInput) {
try {
const oldRelations = {
parents: [],
children: [],
};
const result = await createTag({
variables: { input },
const oldRelations = {
parents: [],
children: [],
};
const result = await createTag({
variables: { input },
});
if (result.data?.tagCreate?.id) {
const created = result.data.tagCreate;
tagRelationHook(created, oldRelations, {
parents: created.parents,
children: created.children,
});
history.push(`/tags/${created.id}`);
Toast.success({
content: intl.formatMessage(
{ id: "toast.created_entity" },
{ entity: intl.formatMessage({ id: "tag" }).toLocaleLowerCase() }
),
});
if (result.data?.tagCreate?.id) {
const created = result.data.tagCreate;
tagRelationHook(created, oldRelations, {
parents: created.parents,
children: created.children,
});
history.push(`/tags/${result.data.tagCreate.id}`);
}
} catch (e) {
Toast.error(e);
}
}

View file

@ -1,4 +1,4 @@
import React, { useEffect } from "react";
import React, { useEffect, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import * as GQL from "src/core/generated-graphql";
import * as yup from "yup";
@ -10,13 +10,14 @@ import ImageUtils from "src/utils/image";
import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
import Mousetrap from "mousetrap";
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
import { StringListInput } from "src/components/Shared/StringListInput";
import isEqual from "lodash-es/isEqual";
import { useToast } from "src/hooks/Toast";
interface ITagEditPanel {
tag: Partial<GQL.TagDataFragment>;
// returns id
onSubmit: (tag: GQL.TagCreateInput) => void;
onSubmit: (tag: GQL.TagCreateInput) => Promise<void>;
onCancel: () => void;
onDelete: () => void;
setImage: (image?: string | null) => void;
@ -32,9 +33,13 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
setEncodingImage,
}) => {
const intl = useIntl();
const Toast = useToast();
const isNew = tag.id === undefined;
// Network state
const [isLoading, setIsLoading] = useState(false);
const labelXS = 3;
const labelXL = 3;
const fieldXS = 9;
@ -84,23 +89,33 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
initialValues,
validationSchema: schema,
enableReinitialize: true,
onSubmit: (values) => onSubmit(values),
onSubmit: (values) => onSave(values),
});
function onCancelEditing() {
setImage(undefined);
onCancel?.();
}
// set up hotkeys
useEffect(() => {
Mousetrap.bind("s s", () => formik.handleSubmit());
Mousetrap.bind("s s", () => {
if (formik.dirty) {
formik.submitForm();
}
});
return () => {
Mousetrap.unbind("s s");
};
});
async function onSave(input: InputValues) {
setIsLoading(true);
try {
await onSubmit(input);
formik.resetForm();
} catch (e) {
Toast.error(e);
}
setIsLoading(false);
}
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
useEffect(() => {
@ -127,6 +142,8 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
: undefined;
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
if (isLoading) return <LoadingIndicator />;
const isEditing = true;
// TODO: CSS class
@ -275,7 +292,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
isNew={isNew}
isEditing={isEditing}
onToggleEdit={onCancelEditing}
onToggleEdit={onCancel}
onSave={formik.handleSubmit}
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
onImageChange={onImageChange}

View file

@ -222,8 +222,10 @@ export const useFindGallery = (id: string) => {
const skip = id === "new";
return GQL.useFindGalleryQuery({ variables: { id }, skip });
};
export const useFindScene = (id: string) =>
GQL.useFindSceneQuery({ variables: { id } });
export const useFindScene = (id: string) => {
const skip = id === "new";
return GQL.useFindSceneQuery({ variables: { id }, skip });
};
export const useSceneStreams = (id: string) =>
GQL.useSceneStreamsQuery({ variables: { id } });