mirror of
https://github.com/stashapp/stash.git
synced 2025-12-07 17:02:38 +01:00
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:
parent
fc53380310
commit
d0847d1ebf
21 changed files with 436 additions and 332 deletions
|
|
@ -64,6 +64,23 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
||||||
|
|
||||||
const [organizedLoading, setOrganizedLoading] = useState(false);
|
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 () => {
|
const onOrganizedClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOrganizedLoading(true);
|
setOrganizedLoading(true);
|
||||||
|
|
@ -242,6 +259,7 @@ export const GalleryPage: React.FC<IProps> = ({ gallery }) => {
|
||||||
<GalleryEditPanel
|
<GalleryEditPanel
|
||||||
isVisible={activeTabKey === "gallery-edit-panel"}
|
isVisible={activeTabKey === "gallery-edit-panel"}
|
||||||
gallery={gallery}
|
gallery={gallery}
|
||||||
|
onSubmit={onSave}
|
||||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||||
/>
|
/>
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,39 @@
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
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";
|
import { GalleryEditPanel } from "./GalleryEditPanel";
|
||||||
|
|
||||||
const GalleryCreate: React.FC = () => {
|
const GalleryCreate: React.FC = () => {
|
||||||
|
const history = useHistory();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
const gallery = {
|
const gallery = {
|
||||||
title: query.get("q") ?? undefined,
|
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 (
|
return (
|
||||||
<div className="row new-view">
|
<div className="row new-view">
|
||||||
<div className="col-md-6">
|
<div className="col-md-6">
|
||||||
|
|
@ -20,7 +43,12 @@ const GalleryCreate: React.FC = () => {
|
||||||
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
values={{ entityType: intl.formatMessage({ id: "gallery" }) }}
|
||||||
/>
|
/>
|
||||||
</h2>
|
</h2>
|
||||||
<GalleryEditPanel gallery={gallery} isVisible onDelete={() => {}} />
|
<GalleryEditPanel
|
||||||
|
gallery={gallery}
|
||||||
|
isVisible
|
||||||
|
onSubmit={onSave}
|
||||||
|
onDelete={() => {}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useHistory, Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
Dropdown,
|
Dropdown,
|
||||||
|
|
@ -15,8 +15,6 @@ import * as yup from "yup";
|
||||||
import {
|
import {
|
||||||
queryScrapeGallery,
|
queryScrapeGallery,
|
||||||
queryScrapeGalleryURL,
|
queryScrapeGalleryURL,
|
||||||
useGalleryCreate,
|
|
||||||
useGalleryUpdate,
|
|
||||||
useListGalleryScrapers,
|
useListGalleryScrapers,
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
|
|
@ -44,17 +42,18 @@ import { DateInput } from "src/components/Shared/DateInput";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
onSubmit: (input: GQL.GalleryCreateInput) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const GalleryEditPanel: React.FC<IProps> = ({
|
export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
gallery,
|
gallery,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
onSubmit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
|
||||||
const [scenes, setScenes] = useState<{ id: string; title: string }[]>(
|
const [scenes, setScenes] = useState<{ id: string; title: string }[]>(
|
||||||
(gallery?.scenes ?? []).map((s) => ({
|
(gallery?.scenes ?? []).map((s) => ({
|
||||||
id: s.id,
|
id: s.id,
|
||||||
|
|
@ -74,9 +73,6 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const [createGallery] = useGalleryCreate();
|
|
||||||
const [updateGallery] = useGalleryUpdate();
|
|
||||||
|
|
||||||
const titleRequired =
|
const titleRequired =
|
||||||
isNew || (gallery?.files?.length === 0 && !gallery?.folder);
|
isNew || (gallery?.files?.length === 0 && !gallery?.folder);
|
||||||
|
|
||||||
|
|
@ -151,7 +147,9 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
formik.handleSubmit();
|
if (formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
|
|
@ -174,51 +172,11 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
||||||
setQueryableScrapers(newQueryableScrapers);
|
setQueryableScrapers(newQueryableScrapers);
|
||||||
}, [Scrapers]);
|
}, [Scrapers]);
|
||||||
|
|
||||||
async function onSave(input: GQL.GalleryCreateInput) {
|
async function onSave(input: InputValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (isNew) {
|
await onSubmit(input);
|
||||||
const result = await createGallery({
|
formik.resetForm();
|
||||||
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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,7 @@ import { Icon } from "src/components/Shared/Icon";
|
||||||
import { Counter } from "src/components/Shared/Counter";
|
import { Counter } from "src/components/Shared/Counter";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import * as Mousetrap from "mousetrap";
|
import * as Mousetrap from "mousetrap";
|
||||||
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
import { OCounterButton } from "src/components/Scenes/SceneDetails/OCounterButton";
|
||||||
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
import { OrganizedButton } from "src/components/Scenes/SceneDetails/OrganizedButton";
|
||||||
import { ImageFileInfoPanel } from "./ImageFileInfoPanel";
|
import { ImageFileInfoPanel } from "./ImageFileInfoPanel";
|
||||||
|
|
@ -51,6 +52,18 @@ export const Image: React.FC = () => {
|
||||||
|
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
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() {
|
async function onRescan() {
|
||||||
if (!image || !image.visual_files.length) {
|
if (!image || !image.visual_files.length) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -225,6 +238,7 @@ export const Image: React.FC = () => {
|
||||||
<ImageEditPanel
|
<ImageEditPanel
|
||||||
isVisible={activeTabKey === "image-edit-panel"}
|
isVisible={activeTabKey === "image-edit-panel"}
|
||||||
image={image}
|
image={image}
|
||||||
|
onSubmit={onSave}
|
||||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||||
/>
|
/>
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import { useImageUpdate } from "src/core/StashService";
|
|
||||||
import {
|
import {
|
||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
TagSelect,
|
TagSelect,
|
||||||
|
|
@ -25,12 +24,14 @@ import { DateInput } from "src/components/Shared/DateInput";
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
onSubmit: (input: GQL.ImageUpdateInput) => Promise<void>;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ImageEditPanel: React.FC<IProps> = ({
|
export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
image,
|
image,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
onSubmit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
@ -41,8 +42,6 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
|
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
const [updateImage] = useImageUpdate();
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
|
|
@ -97,7 +96,9 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
formik.handleSubmit();
|
if (formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
|
|
@ -113,23 +114,11 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await updateImage({
|
await onSubmit({
|
||||||
variables: {
|
id: image.id,
|
||||||
input: {
|
...input,
|
||||||
id: image.id,
|
|
||||||
...input,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (result.data?.imageUpdate) {
|
formik.resetForm();
|
||||||
Toast.success({
|
|
||||||
content: intl.formatMessage(
|
|
||||||
{ id: "toast.updated_entity" },
|
|
||||||
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
|
|
||||||
),
|
|
||||||
});
|
|
||||||
formik.resetForm();
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,7 +83,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("e", () => setIsEditing(true));
|
Mousetrap.bind("e", () => toggleEditing());
|
||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
});
|
});
|
||||||
|
|
@ -95,22 +95,21 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSave(input: GQL.MovieCreateInput) {
|
async function onSave(input: GQL.MovieCreateInput) {
|
||||||
try {
|
await updateMovie({
|
||||||
const result = await updateMovie({
|
variables: {
|
||||||
variables: {
|
input: {
|
||||||
input: {
|
id: movie.id,
|
||||||
id: movie.id,
|
...input,
|
||||||
...input,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
if (result.data?.movieUpdate) {
|
});
|
||||||
setIsEditing(false);
|
toggleEditing(false);
|
||||||
history.push(`/movies/${result.data.movieUpdate.id}`);
|
Toast.success({
|
||||||
}
|
content: intl.formatMessage(
|
||||||
} catch (e) {
|
{ id: "toast.updated_entity" },
|
||||||
Toast.error(e);
|
{ entity: intl.formatMessage({ id: "movie" }).toLocaleLowerCase() }
|
||||||
}
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onDelete() {
|
async function onDelete() {
|
||||||
|
|
@ -124,8 +123,12 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||||
history.push(`/movies`);
|
history.push(`/movies`);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleEdit() {
|
function toggleEditing(value?: boolean) {
|
||||||
setIsEditing(!isEditing);
|
if (value !== undefined) {
|
||||||
|
setIsEditing(value);
|
||||||
|
} else {
|
||||||
|
setIsEditing((e) => !e);
|
||||||
|
}
|
||||||
setFrontImage(undefined);
|
setFrontImage(undefined);
|
||||||
setBackImage(undefined);
|
setBackImage(undefined);
|
||||||
}
|
}
|
||||||
|
|
@ -239,7 +242,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||||
objectName={movie.name}
|
objectName={movie.name}
|
||||||
isNew={false}
|
isNew={false}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onToggleEdit}
|
onToggleEdit={() => toggleEditing()}
|
||||||
onSave={() => {}}
|
onSave={() => {}}
|
||||||
onImageChange={() => {}}
|
onImageChange={() => {}}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
|
|
@ -249,7 +252,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
||||||
<MovieEditPanel
|
<MovieEditPanel
|
||||||
movie={movie}
|
movie={movie}
|
||||||
onSubmit={onSave}
|
onSubmit={onSave}
|
||||||
onCancel={onToggleEdit}
|
onCancel={() => toggleEditing()}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
setFrontImage={setFrontImage}
|
setFrontImage={setFrontImage}
|
||||||
setBackImage={setBackImage}
|
setBackImage={setBackImage}
|
||||||
|
|
|
||||||
|
|
@ -2,15 +2,17 @@ import React, { useMemo, useState } from "react";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useMovieCreate } from "src/core/StashService";
|
import { useMovieCreate } from "src/core/StashService";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { MovieEditPanel } from "./MovieEditPanel";
|
import { MovieEditPanel } from "./MovieEditPanel";
|
||||||
|
|
||||||
const MovieCreate: React.FC = () => {
|
const MovieCreate: React.FC = () => {
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
const movie = {
|
const movie = {
|
||||||
name: query.get("q") ?? undefined,
|
name: query.get("q") ?? undefined,
|
||||||
|
|
@ -24,15 +26,17 @@ const MovieCreate: React.FC = () => {
|
||||||
const [createMovie] = useMovieCreate();
|
const [createMovie] = useMovieCreate();
|
||||||
|
|
||||||
async function onSave(input: GQL.MovieCreateInput) {
|
async function onSave(input: GQL.MovieCreateInput) {
|
||||||
try {
|
const result = await createMovie({
|
||||||
const result = await createMovie({
|
variables: input,
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ import { DateInput } from "src/components/Shared/DateInput";
|
||||||
|
|
||||||
interface IMovieEditPanel {
|
interface IMovieEditPanel {
|
||||||
movie: Partial<GQL.MovieDataFragment>;
|
movie: Partial<GQL.MovieDataFragment>;
|
||||||
onSubmit: (movie: GQL.MovieCreateInput) => void;
|
onSubmit: (movie: GQL.MovieCreateInput) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
setFrontImage: (image?: string | null) => void;
|
setFrontImage: (image?: string | null) => void;
|
||||||
|
|
@ -103,7 +103,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
onSubmit: (values) => onSubmit(values),
|
onSubmit: (values) => onSave(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
|
|
@ -116,19 +116,17 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||||
setRating
|
setRating
|
||||||
);
|
);
|
||||||
|
|
||||||
function onCancelEditing() {
|
|
||||||
setFrontImage(undefined);
|
|
||||||
setBackImage(undefined);
|
|
||||||
onCancel?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Mousetrap.bind("u", (e) => {
|
// Mousetrap.bind("u", (e) => {
|
||||||
// setStudioFocus()
|
// setStudioFocus()
|
||||||
// e.preventDefault();
|
// e.preventDefault();
|
||||||
// });
|
// });
|
||||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
Mousetrap.bind("s s", () => {
|
||||||
|
if (formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
// Mousetrap.unbind("u");
|
// 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() {
|
async function onScrapeMovieURL() {
|
||||||
const { url } = formik.values;
|
const { url } = formik.values;
|
||||||
if (!url) return;
|
if (!url) return;
|
||||||
|
|
@ -488,7 +497,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||||
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
|
objectName={movie?.name ?? intl.formatMessage({ id: "movie" })}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onCancelEditing}
|
onToggleEdit={onCancel}
|
||||||
onSave={formik.handleSubmit}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onImageChange={onFrontImageChange}
|
onImageChange={onFrontImageChange}
|
||||||
|
|
|
||||||
|
|
@ -68,15 +68,17 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||||
|
|
||||||
const activeImage = useMemo(() => {
|
const activeImage = useMemo(() => {
|
||||||
const performerImage = performer.image_path;
|
const performerImage = performer.image_path;
|
||||||
if (image === null && performerImage) {
|
if (isEditing) {
|
||||||
const performerImageURL = new URL(performerImage);
|
if (image === null && performerImage) {
|
||||||
performerImageURL.searchParams.set("default", "true");
|
const performerImageURL = new URL(performerImage);
|
||||||
return performerImageURL.toString();
|
performerImageURL.searchParams.set("default", "true");
|
||||||
} else if (image) {
|
return performerImageURL.toString();
|
||||||
return image;
|
} else if (image) {
|
||||||
|
return image;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return performerImage;
|
return performerImage;
|
||||||
}, [image, performer.image_path]);
|
}, [image, isEditing, performer.image_path]);
|
||||||
|
|
||||||
const lightboxImages = useMemo(
|
const lightboxImages = useMemo(
|
||||||
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||||
|
|
@ -122,15 +124,10 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||||
setRating
|
setRating
|
||||||
);
|
);
|
||||||
|
|
||||||
// reset image if performer changed
|
|
||||||
useEffect(() => {
|
|
||||||
setImage(undefined);
|
|
||||||
}, [performer]);
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("a", () => setActiveTabKey("details"));
|
Mousetrap.bind("a", () => setActiveTabKey("details"));
|
||||||
Mousetrap.bind("e", () => setIsEditing(!isEditing));
|
Mousetrap.bind("e", () => toggleEditing());
|
||||||
Mousetrap.bind("c", () => setActiveTabKey("scenes"));
|
Mousetrap.bind("c", () => setActiveTabKey("scenes"));
|
||||||
Mousetrap.bind("g", () => setActiveTabKey("galleries"));
|
Mousetrap.bind("g", () => setActiveTabKey("galleries"));
|
||||||
Mousetrap.bind("m", () => setActiveTabKey("movies"));
|
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() {
|
async function onDelete() {
|
||||||
try {
|
try {
|
||||||
await deletePerformer({ variables: { id: performer.id } });
|
await deletePerformer({ variables: { id: performer.id } });
|
||||||
|
|
@ -158,6 +173,15 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||||
history.push("/performers");
|
history.push("/performers");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function toggleEditing(value?: boolean) {
|
||||||
|
if (value !== undefined) {
|
||||||
|
setIsEditing(value);
|
||||||
|
} else {
|
||||||
|
setIsEditing((e) => !e);
|
||||||
|
}
|
||||||
|
setImage(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
function renderImage() {
|
function renderImage() {
|
||||||
if (activeImage) {
|
if (activeImage) {
|
||||||
return (
|
return (
|
||||||
|
|
@ -175,9 +199,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||||
objectName={
|
objectName={
|
||||||
performer?.name ?? intl.formatMessage({ id: "performer" })
|
performer?.name ?? intl.formatMessage({ id: "performer" })
|
||||||
}
|
}
|
||||||
onToggleEdit={() => {
|
onToggleEdit={() => toggleEditing()}
|
||||||
setIsEditing(!isEditing);
|
|
||||||
}}
|
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onAutoTag={onAutoTag}
|
onAutoTag={onAutoTag}
|
||||||
isNew={false}
|
isNew={false}
|
||||||
|
|
@ -297,7 +319,8 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isVisible={isEditing}
|
isVisible={isEditing}
|
||||||
onCancel={() => setIsEditing(false)}
|
onSubmit={onSave}
|
||||||
|
onCancel={() => toggleEditing()}
|
||||||
setImage={setImage}
|
setImage={setImage}
|
||||||
setEncodingImage={setEncodingImage}
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -2,9 +2,16 @@ import React, { useMemo, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { PerformerEditPanel } from "./PerformerEditPanel";
|
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 PerformerCreate: React.FC = () => {
|
||||||
|
const Toast = useToast();
|
||||||
|
const history = useHistory();
|
||||||
|
const intl = useIntl();
|
||||||
|
|
||||||
const [image, setImage] = useState<string | null>();
|
const [image, setImage] = useState<string | null>();
|
||||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
|
|
@ -14,7 +21,24 @@ const PerformerCreate: React.FC = () => {
|
||||||
name: query.get("q") ?? undefined,
|
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() {
|
function renderPerformerImage() {
|
||||||
if (encodingImage) {
|
if (encodingImage) {
|
||||||
|
|
@ -46,6 +70,7 @@ const PerformerCreate: React.FC = () => {
|
||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isVisible
|
isVisible
|
||||||
|
onSubmit={onSave}
|
||||||
setImage={setImage}
|
setImage={setImage}
|
||||||
setEncodingImage={setEncodingImage}
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -8,8 +8,6 @@ import {
|
||||||
useListPerformerScrapers,
|
useListPerformerScrapers,
|
||||||
queryScrapePerformer,
|
queryScrapePerformer,
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
usePerformerUpdate,
|
|
||||||
usePerformerCreate,
|
|
||||||
useTagCreate,
|
useTagCreate,
|
||||||
queryScrapePerformerURL,
|
queryScrapePerformerURL,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
|
|
@ -24,7 +22,7 @@ import ImageUtils from "src/utils/image";
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { getStashIDs } from "src/utils/stashIds";
|
||||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||||
import { useToast } from "src/hooks/Toast";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { Prompt, useHistory } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import {
|
import {
|
||||||
genderToString,
|
genderToString,
|
||||||
|
|
@ -57,6 +55,7 @@ const isScraper = (
|
||||||
interface IPerformerDetails {
|
interface IPerformerDetails {
|
||||||
performer: Partial<GQL.PerformerDataFragment>;
|
performer: Partial<GQL.PerformerDataFragment>;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
onSubmit: (performer: GQL.PerformerCreateInput) => Promise<void>;
|
||||||
onCancel?: () => void;
|
onCancel?: () => void;
|
||||||
setImage: (image?: string | null) => void;
|
setImage: (image?: string | null) => void;
|
||||||
setEncodingImage: (loading: boolean) => void;
|
setEncodingImage: (loading: boolean) => void;
|
||||||
|
|
@ -65,12 +64,12 @@ interface IPerformerDetails {
|
||||||
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
performer,
|
performer,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
setImage,
|
setImage,
|
||||||
setEncodingImage,
|
setEncodingImage,
|
||||||
}) => {
|
}) => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const isNew = performer.id === undefined;
|
const isNew = performer.id === undefined;
|
||||||
|
|
||||||
|
|
@ -82,9 +81,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const [updatePerformer] = usePerformerUpdate();
|
|
||||||
const [createPerformer] = usePerformerCreate();
|
|
||||||
|
|
||||||
const Scrapers = useListPerformerScrapers();
|
const Scrapers = useListPerformerScrapers();
|
||||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||||
|
|
||||||
|
|
@ -454,61 +450,35 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
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) {
|
async function onSave(input: InputValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (isNew) {
|
await onSubmit(valuesToInput(input));
|
||||||
const result = await createPerformer({
|
formik.resetForm();
|
||||||
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,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
setIsLoading(false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (!isNew && onCancel) {
|
|
||||||
onCancel();
|
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCancelEditing() {
|
|
||||||
setImage(undefined);
|
|
||||||
onCancel?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
onSave?.(formik.values);
|
if (formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
|
|
@ -699,9 +669,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPerformer = {
|
const currentPerformer = {
|
||||||
...formik.values,
|
...valuesToInput(formik.values),
|
||||||
gender: formik.values.gender || null,
|
|
||||||
circumcised: formik.values.circumcised || null,
|
|
||||||
image: formik.values.image ?? performer.image_path,
|
image: formik.values.image ?? performer.image_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -729,7 +697,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
return (
|
return (
|
||||||
<div className={cx("details-edit", "col-xl-9", classNames)}>
|
<div className={cx("details-edit", "col-xl-9", classNames)}>
|
||||||
{!isNew && onCancel ? (
|
{!isNew && onCancel ? (
|
||||||
<Button className="mr-2" variant="primary" onClick={onCancelEditing}>
|
<Button className="mr-2" variant="primary" onClick={onCancel}>
|
||||||
<FormattedMessage id="actions.cancel" />
|
<FormattedMessage id="actions.cancel" />
|
||||||
</Button>
|
</Button>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
const onOrganizedClick = async () => {
|
||||||
try {
|
try {
|
||||||
setOrganizedLoading(true);
|
setOrganizedLoading(true);
|
||||||
|
|
@ -461,6 +478,7 @@ const ScenePage: React.FC<IProps> = ({
|
||||||
<SceneEditPanel
|
<SceneEditPanel
|
||||||
isVisible={activeTabKey === "scene-edit-panel"}
|
isVisible={activeTabKey === "scene-edit-panel"}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
|
onSubmit={onSave}
|
||||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||||
/>
|
/>
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,23 @@
|
||||||
import React, { useEffect, useMemo, useState } from "react";
|
import React, { useEffect, useMemo, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import { useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
import { SceneEditPanel } from "./SceneEditPanel";
|
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 ImageUtils from "src/utils/image";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
|
||||||
const SceneCreate: React.FC = () => {
|
const SceneCreate: React.FC = () => {
|
||||||
|
const history = useHistory();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
|
|
||||||
// create scene from provided scene id if applicable
|
// 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 [loadingCoverImage, setLoadingCoverImage] = useState(false);
|
||||||
const [coverImage, setCoverImage] = useState<string>();
|
const [coverImage, setCoverImage] = useState<string>();
|
||||||
|
|
||||||
|
|
@ -53,6 +57,23 @@ const SceneCreate: React.FC = () => {
|
||||||
return <LoadingIndicator />;
|
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 (
|
return (
|
||||||
<div className="row new-view justify-content-center" id="create-scene-page">
|
<div className="row new-view justify-content-center" id="create-scene-page">
|
||||||
<div className="col-md-8">
|
<div className="col-md-8">
|
||||||
|
|
@ -64,10 +85,10 @@ const SceneCreate: React.FC = () => {
|
||||||
</h2>
|
</h2>
|
||||||
<SceneEditPanel
|
<SceneEditPanel
|
||||||
scene={scene}
|
scene={scene}
|
||||||
fileID={query.get("file_id") ?? undefined}
|
|
||||||
initialCoverImage={coverImage}
|
initialCoverImage={coverImage}
|
||||||
isVisible
|
isVisible
|
||||||
isNew
|
isNew
|
||||||
|
onSubmit={onSave}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,10 +16,8 @@ import {
|
||||||
queryScrapeScene,
|
queryScrapeScene,
|
||||||
queryScrapeSceneURL,
|
queryScrapeSceneURL,
|
||||||
useListSceneScrapers,
|
useListSceneScrapers,
|
||||||
useSceneUpdate,
|
|
||||||
mutateReloadScrapers,
|
mutateReloadScrapers,
|
||||||
queryScrapeSceneQueryFragment,
|
queryScrapeSceneQueryFragment,
|
||||||
mutateCreateScene,
|
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import {
|
import {
|
||||||
PerformerSelect,
|
PerformerSelect,
|
||||||
|
|
@ -37,7 +35,7 @@ import ImageUtils from "src/utils/image";
|
||||||
import FormUtils from "src/utils/form";
|
import FormUtils from "src/utils/form";
|
||||||
import { getStashIDs } from "src/utils/stashIds";
|
import { getStashIDs } from "src/utils/stashIds";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt, useHistory } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import { stashboxDisplayName } from "src/utils/stashbox";
|
import { stashboxDisplayName } from "src/utils/stashbox";
|
||||||
import { SceneMovieTable } from "./SceneMovieTable";
|
import { SceneMovieTable } from "./SceneMovieTable";
|
||||||
|
|
@ -59,24 +57,23 @@ const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
scene: Partial<GQL.SceneDataFragment>;
|
scene: Partial<GQL.SceneDataFragment>;
|
||||||
fileID?: string;
|
|
||||||
initialCoverImage?: string;
|
initialCoverImage?: string;
|
||||||
isNew?: boolean;
|
isNew?: boolean;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
|
onSubmit: (input: GQL.SceneCreateInput) => Promise<void>;
|
||||||
onDelete?: () => void;
|
onDelete?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneEditPanel: React.FC<IProps> = ({
|
export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
scene,
|
scene,
|
||||||
fileID,
|
|
||||||
initialCoverImage,
|
initialCoverImage,
|
||||||
isNew = false,
|
isNew = false,
|
||||||
isVisible,
|
isVisible,
|
||||||
|
onSubmit,
|
||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
|
||||||
|
|
||||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
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 [scrapedScene, setScrapedScene] = useState<GQL.ScrapedScene | null>();
|
||||||
const [endpoint, setEndpoint] = useState<string>();
|
const [endpoint, setEndpoint] = useState<string>();
|
||||||
|
|
||||||
const [coverImagePreview, setCoverImagePreview] = useState<string>();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setCoverImagePreview(
|
|
||||||
initialCoverImage ?? scene.paths?.screenshot ?? undefined
|
|
||||||
);
|
|
||||||
}, [scene.paths?.screenshot, initialCoverImage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setGalleries(
|
setGalleries(
|
||||||
scene.galleries?.map((g) => ({
|
scene.galleries?.map((g) => ({
|
||||||
|
|
@ -114,8 +103,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
// Network state
|
// Network state
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const [updateScene] = useSceneUpdate();
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().ensure(),
|
title: yup.string().ensure(),
|
||||||
code: yup.string().ensure(),
|
code: yup.string().ensure(),
|
||||||
|
|
@ -183,6 +170,19 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
onSubmit: (values) => onSave(values),
|
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) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating100", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
|
|
@ -209,7 +209,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
formik.handleSubmit();
|
if (formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
});
|
});
|
||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
if (onDelete) {
|
if (onDelete) {
|
||||||
|
|
@ -259,35 +261,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!isNew) {
|
await onSubmit(input);
|
||||||
const result = await updateScene({
|
formik.resetForm();
|
||||||
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}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
|
|
@ -318,7 +293,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
function onImageLoad(imageData: string) {
|
||||||
setCoverImagePreview(imageData);
|
|
||||||
formik.setFieldValue("cover_image", imageData);
|
formik.setFieldValue("cover_image", imageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -619,7 +593,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
if (updatedScene.image) {
|
if (updatedScene.image) {
|
||||||
// image is a base64 string
|
// image is a base64 string
|
||||||
formik.setFieldValue("cover_image", updatedScene.image);
|
formik.setFieldValue("cover_image", updatedScene.image);
|
||||||
setCoverImagePreview(updatedScene.image);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.remote_site_id && endpoint) {
|
if (updatedScene.remote_site_id && endpoint) {
|
||||||
|
|
|
||||||
|
|
@ -69,7 +69,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("e", () => setIsEditing(true));
|
Mousetrap.bind("e", () => toggleEditing());
|
||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
});
|
});
|
||||||
|
|
@ -83,21 +83,21 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSave(input: GQL.StudioCreateInput) {
|
async function onSave(input: GQL.StudioCreateInput) {
|
||||||
try {
|
await updateStudio({
|
||||||
const result = await updateStudio({
|
variables: {
|
||||||
variables: {
|
input: {
|
||||||
input: {
|
id: studio.id,
|
||||||
id: studio.id,
|
...input,
|
||||||
...input,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
});
|
},
|
||||||
if (result.data?.studioUpdate) {
|
});
|
||||||
setIsEditing(false);
|
toggleEditing(false);
|
||||||
}
|
Toast.success({
|
||||||
} catch (e) {
|
content: intl.formatMessage(
|
||||||
Toast.error(e);
|
{ id: "toast.updated_entity" },
|
||||||
}
|
{ entity: intl.formatMessage({ id: "studio" }).toLocaleLowerCase() }
|
||||||
|
),
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onAutoTag() {
|
async function onAutoTag() {
|
||||||
|
|
@ -149,8 +149,13 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onToggleEdit() {
|
function toggleEditing(value?: boolean) {
|
||||||
setIsEditing(!isEditing);
|
if (value !== undefined) {
|
||||||
|
setIsEditing(value);
|
||||||
|
} else {
|
||||||
|
setIsEditing((e) => !e);
|
||||||
|
}
|
||||||
|
setImage(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImage() {
|
function renderImage() {
|
||||||
|
|
@ -213,7 +218,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||||
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
objectName={studio.name ?? intl.formatMessage({ id: "studio" })}
|
||||||
isNew={false}
|
isNew={false}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onToggleEdit}
|
onToggleEdit={() => toggleEditing()}
|
||||||
onSave={() => {}}
|
onSave={() => {}}
|
||||||
onImageChange={() => {}}
|
onImageChange={() => {}}
|
||||||
onClearImage={() => {}}
|
onClearImage={() => {}}
|
||||||
|
|
@ -225,7 +230,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
||||||
<StudioEditPanel
|
<StudioEditPanel
|
||||||
studio={studio}
|
studio={studio}
|
||||||
onSubmit={onSave}
|
onSubmit={onSave}
|
||||||
onCancel={onToggleEdit}
|
onCancel={() => toggleEditing()}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
setImage={setImage}
|
setImage={setImage}
|
||||||
setEncodingImage={setEncodingImage}
|
setEncodingImage={setEncodingImage}
|
||||||
|
|
|
||||||
|
|
@ -27,15 +27,17 @@ const StudioCreate: React.FC = () => {
|
||||||
const [createStudio] = useStudioCreate();
|
const [createStudio] = useStudioCreate();
|
||||||
|
|
||||||
async function onSave(input: GQL.StudioCreateInput) {
|
async function onSave(input: GQL.StudioCreateInput) {
|
||||||
try {
|
const result = await createStudio({
|
||||||
const result = await createStudio({
|
variables: { input },
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import { Icon } from "src/components/Shared/Icon";
|
import { Icon } from "src/components/Shared/Icon";
|
||||||
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { StudioSelect } from "src/components/Shared/Select";
|
import { StudioSelect } from "src/components/Shared/Select";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { Button, Form, Col, Row } from "react-bootstrap";
|
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 { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
|
||||||
interface IStudioEditPanel {
|
interface IStudioEditPanel {
|
||||||
studio: Partial<GQL.StudioDataFragment>;
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
onSubmit: (studio: GQL.StudioCreateInput) => void;
|
onSubmit: (studio: GQL.StudioCreateInput) => Promise<void>;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
setImage: (image?: string | null) => void;
|
setImage: (image?: string | null) => void;
|
||||||
|
|
@ -37,10 +39,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||||
setEncodingImage,
|
setEncodingImage,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
const isNew = studio.id === undefined;
|
const isNew = studio.id === undefined;
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
url: yup.string().ensure(),
|
url: yup.string().ensure(),
|
||||||
|
|
@ -73,6 +79,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
|
id: studio.id,
|
||||||
name: studio.name ?? "",
|
name: studio.name ?? "",
|
||||||
url: studio.url ?? "",
|
url: studio.url ?? "",
|
||||||
details: studio.details ?? "",
|
details: studio.details ?? "",
|
||||||
|
|
@ -89,7 +96,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
onSubmit: (values) => onSubmit(values),
|
onSubmit: (values) => onSave(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
||||||
|
|
@ -114,20 +121,30 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||||
setRating
|
setRating
|
||||||
);
|
);
|
||||||
|
|
||||||
function onCancelEditing() {
|
|
||||||
setImage(undefined);
|
|
||||||
onCancel?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
Mousetrap.bind("s s", () => {
|
||||||
|
if (formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("s s");
|
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) {
|
function onImageLoad(imageData: string | null) {
|
||||||
formik.setFieldValue("image", imageData);
|
formik.setFieldValue("image", imageData);
|
||||||
}
|
}
|
||||||
|
|
@ -200,6 +217,8 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||||
: undefined;
|
: undefined;
|
||||||
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
||||||
|
|
||||||
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Prompt
|
<Prompt
|
||||||
|
|
@ -331,7 +350,7 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||||
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
|
objectName={studio?.name ?? intl.formatMessage({ id: "studio" })}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing
|
isEditing
|
||||||
onToggleEdit={onCancelEditing}
|
onToggleEdit={onCancel}
|
||||||
onSave={formik.handleSubmit}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChange}
|
||||||
|
|
|
||||||
|
|
@ -88,7 +88,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("e", () => setIsEditing(true));
|
Mousetrap.bind("e", () => toggleEditing());
|
||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
});
|
});
|
||||||
|
|
@ -106,30 +106,31 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||||
});
|
});
|
||||||
|
|
||||||
async function onSave(input: GQL.TagCreateInput) {
|
async function onSave(input: GQL.TagCreateInput) {
|
||||||
try {
|
const oldRelations = {
|
||||||
const oldRelations = {
|
parents: tag.parents ?? [],
|
||||||
parents: tag.parents ?? [],
|
children: tag.children ?? [],
|
||||||
children: tag.children ?? [],
|
};
|
||||||
};
|
const result = await updateTag({
|
||||||
const result = await updateTag({
|
variables: {
|
||||||
variables: {
|
input: {
|
||||||
input: {
|
id: tag.id,
|
||||||
id: tag.id,
|
...input,
|
||||||
...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() {
|
function toggleEditing(value?: boolean) {
|
||||||
setIsEditing(!isEditing);
|
if (value !== undefined) {
|
||||||
|
setIsEditing(value);
|
||||||
|
} else {
|
||||||
|
setIsEditing((e) => !e);
|
||||||
|
}
|
||||||
setImage(undefined);
|
setImage(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -283,7 +288,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||||
objectName={tag.name}
|
objectName={tag.name}
|
||||||
isNew={false}
|
isNew={false}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onToggleEdit}
|
onToggleEdit={() => toggleEditing()}
|
||||||
onSave={() => {}}
|
onSave={() => {}}
|
||||||
onImageChange={() => {}}
|
onImageChange={() => {}}
|
||||||
onClearImage={() => {}}
|
onClearImage={() => {}}
|
||||||
|
|
@ -297,7 +302,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
||||||
<TagEditPanel
|
<TagEditPanel
|
||||||
tag={tag}
|
tag={tag}
|
||||||
onSubmit={onSave}
|
onSubmit={onSave}
|
||||||
onCancel={onToggleEdit}
|
onCancel={() => toggleEditing()}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
setImage={setImage}
|
setImage={setImage}
|
||||||
setEncodingImage={setEncodingImage}
|
setEncodingImage={setEncodingImage}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useMemo, useState } from "react";
|
import React, { useMemo, useState } from "react";
|
||||||
import { useHistory, useLocation } from "react-router-dom";
|
import { useHistory, useLocation } from "react-router-dom";
|
||||||
|
import { useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useTagCreate } from "src/core/StashService";
|
import { useTagCreate } from "src/core/StashService";
|
||||||
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
|
|
@ -9,10 +9,11 @@ import { tagRelationHook } from "src/core/tags";
|
||||||
import { TagEditPanel } from "./TagEditPanel";
|
import { TagEditPanel } from "./TagEditPanel";
|
||||||
|
|
||||||
const TagCreate: React.FC = () => {
|
const TagCreate: React.FC = () => {
|
||||||
|
const intl = useIntl();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
const location = useLocation();
|
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
|
const location = useLocation();
|
||||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
const tag = {
|
const tag = {
|
||||||
name: query.get("q") ?? undefined,
|
name: query.get("q") ?? undefined,
|
||||||
|
|
@ -25,24 +26,26 @@ const TagCreate: React.FC = () => {
|
||||||
const [createTag] = useTagCreate();
|
const [createTag] = useTagCreate();
|
||||||
|
|
||||||
async function onSave(input: GQL.TagCreateInput) {
|
async function onSave(input: GQL.TagCreateInput) {
|
||||||
try {
|
const oldRelations = {
|
||||||
const oldRelations = {
|
parents: [],
|
||||||
parents: [],
|
children: [],
|
||||||
children: [],
|
};
|
||||||
};
|
const result = await createTag({
|
||||||
const result = await createTag({
|
variables: { input },
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import { FormattedMessage, useIntl } from "react-intl";
|
import { FormattedMessage, useIntl } from "react-intl";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import * as yup from "yup";
|
import * as yup from "yup";
|
||||||
|
|
@ -10,13 +10,14 @@ import ImageUtils from "src/utils/image";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import { Prompt } from "react-router-dom";
|
import { Prompt } from "react-router-dom";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
|
import { LoadingIndicator } from "src/components/Shared/LoadingIndicator";
|
||||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||||
import isEqual from "lodash-es/isEqual";
|
import isEqual from "lodash-es/isEqual";
|
||||||
|
import { useToast } from "src/hooks/Toast";
|
||||||
|
|
||||||
interface ITagEditPanel {
|
interface ITagEditPanel {
|
||||||
tag: Partial<GQL.TagDataFragment>;
|
tag: Partial<GQL.TagDataFragment>;
|
||||||
// returns id
|
onSubmit: (tag: GQL.TagCreateInput) => Promise<void>;
|
||||||
onSubmit: (tag: GQL.TagCreateInput) => void;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
setImage: (image?: string | null) => void;
|
setImage: (image?: string | null) => void;
|
||||||
|
|
@ -32,9 +33,13 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
setEncodingImage,
|
setEncodingImage,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
const Toast = useToast();
|
||||||
|
|
||||||
const isNew = tag.id === undefined;
|
const isNew = tag.id === undefined;
|
||||||
|
|
||||||
|
// Network state
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
|
||||||
const labelXS = 3;
|
const labelXS = 3;
|
||||||
const labelXL = 3;
|
const labelXL = 3;
|
||||||
const fieldXS = 9;
|
const fieldXS = 9;
|
||||||
|
|
@ -84,23 +89,33 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: (values) => onSubmit(values),
|
onSubmit: (values) => onSave(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
function onCancelEditing() {
|
|
||||||
setImage(undefined);
|
|
||||||
onCancel?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
Mousetrap.bind("s s", () => {
|
||||||
|
if (formik.dirty) {
|
||||||
|
formik.submitForm();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
Mousetrap.unbind("s s");
|
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);
|
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -127,6 +142,8 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
: undefined;
|
: undefined;
|
||||||
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
const aliasErrorIdx = aliasErrors?.split(" ").map((e) => parseInt(e));
|
||||||
|
|
||||||
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
const isEditing = true;
|
const isEditing = true;
|
||||||
|
|
||||||
// TODO: CSS class
|
// TODO: CSS class
|
||||||
|
|
@ -275,7 +292,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
|
objectName={tag?.name ?? intl.formatMessage({ id: "tag" })}
|
||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onCancelEditing}
|
onToggleEdit={onCancel}
|
||||||
onSave={formik.handleSubmit}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChange}
|
||||||
|
|
|
||||||
|
|
@ -222,8 +222,10 @@ export const useFindGallery = (id: string) => {
|
||||||
const skip = id === "new";
|
const skip = id === "new";
|
||||||
return GQL.useFindGalleryQuery({ variables: { id }, skip });
|
return GQL.useFindGalleryQuery({ variables: { id }, skip });
|
||||||
};
|
};
|
||||||
export const useFindScene = (id: string) =>
|
export const useFindScene = (id: string) => {
|
||||||
GQL.useFindSceneQuery({ variables: { id } });
|
const skip = id === "new";
|
||||||
|
return GQL.useFindSceneQuery({ variables: { id }, skip });
|
||||||
|
};
|
||||||
export const useSceneStreams = (id: string) =>
|
export const useSceneStreams = (id: string) =>
|
||||||
GQL.useSceneStreamsQuery({ variables: { id } });
|
GQL.useSceneStreamsQuery({ variables: { id } });
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue