mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 12:24:38 +03:00
@@ -21,6 +21,7 @@ import {
|
|||||||
useSystemStatus,
|
useSystemStatus,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import flattenMessages from "./utils/flattenMessages";
|
import flattenMessages from "./utils/flattenMessages";
|
||||||
|
import * as yup from "yup";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import MousetrapPause from "mousetrap-pause";
|
import MousetrapPause from "mousetrap-pause";
|
||||||
import { ErrorBoundary } from "./components/ErrorBoundary";
|
import { ErrorBoundary } from "./components/ErrorBoundary";
|
||||||
@@ -126,7 +127,18 @@ export const App: React.FC = () => {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
setMessages(flattenMessages(mergedMessages));
|
const newMessages = flattenMessages(mergedMessages) as Record<
|
||||||
|
string,
|
||||||
|
string
|
||||||
|
>;
|
||||||
|
|
||||||
|
yup.setLocale({
|
||||||
|
mixed: {
|
||||||
|
required: newMessages["validation.required"],
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
setMessages(newMessages);
|
||||||
};
|
};
|
||||||
|
|
||||||
setLocale();
|
setLocale();
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ import { faSyncAlt } from "@fortawesome/free-solid-svg-icons";
|
|||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
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";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
gallery: Partial<GQL.GalleryDataFragment>;
|
gallery: Partial<GQL.GalleryDataFragment>;
|
||||||
@@ -79,44 +80,50 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
isNew || (gallery?.files?.length === 0 && !gallery?.folder);
|
isNew || (gallery?.files?.length === 0 && !gallery?.folder);
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: titleRequired
|
title: titleRequired ? yup.string().required() : yup.string().ensure(),
|
||||||
? yup.string().required()
|
url: yup.string().ensure(),
|
||||||
: yup.string().optional().nullable(),
|
date: yup
|
||||||
details: yup.string().optional().nullable(),
|
.string()
|
||||||
url: yup.string().optional().nullable(),
|
.ensure()
|
||||||
date: yup.string().optional().nullable(),
|
.test({
|
||||||
rating100: yup.number().optional().nullable(),
|
name: "date",
|
||||||
studio_id: yup.string().optional().nullable(),
|
test: (value) => {
|
||||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
if (!value) return true;
|
||||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||||
scene_ids: yup.array(yup.string().required()).optional().nullable(),
|
if (Number.isNaN(Date.parse(value))) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||||
|
}),
|
||||||
|
rating100: yup.number().nullable().defined(),
|
||||||
|
studio_id: yup.string().required().nullable(),
|
||||||
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
tag_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
scene_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
details: yup.string().ensure(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
title: gallery?.title ?? "",
|
title: gallery?.title ?? "",
|
||||||
details: gallery?.details ?? "",
|
|
||||||
url: gallery?.url ?? "",
|
url: gallery?.url ?? "",
|
||||||
date: gallery?.date ?? "",
|
date: gallery?.date ?? "",
|
||||||
rating100: gallery?.rating100 ?? null,
|
rating100: gallery?.rating100 ?? null,
|
||||||
studio_id: gallery?.studio?.id,
|
studio_id: gallery?.studio?.id ?? null,
|
||||||
performer_ids: (gallery?.performers ?? []).map((p) => p.id),
|
performer_ids: (gallery?.performers ?? []).map((p) => p.id),
|
||||||
tag_ids: (gallery?.tags ?? []).map((t) => t.id),
|
tag_ids: (gallery?.tags ?? []).map((t) => t.id),
|
||||||
scene_ids: (gallery?.scenes ?? []).map((s) => s.id),
|
scene_ids: (gallery?.scenes ?? []).map((s) => s.id),
|
||||||
|
details: gallery?.details ?? "",
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
onSubmit: (values) => onSave(getGalleryInput(values)),
|
onSubmit: (values) => onSave(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
// always dirty if creating a new gallery with a title
|
|
||||||
if (isNew && gallery?.title) {
|
|
||||||
formik.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating100", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
@@ -166,24 +173,13 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
setQueryableScrapers(newQueryableScrapers);
|
setQueryableScrapers(newQueryableScrapers);
|
||||||
}, [Scrapers]);
|
}, [Scrapers]);
|
||||||
|
|
||||||
function getGalleryInput(
|
async function onSave(input: GQL.GalleryCreateInput) {
|
||||||
input: InputValues
|
|
||||||
): GQL.GalleryCreateInput | GQL.GalleryUpdateInput {
|
|
||||||
return {
|
|
||||||
id: isNew ? undefined : gallery?.id ?? "",
|
|
||||||
...input,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(
|
|
||||||
input: GQL.GalleryCreateInput | GQL.GalleryUpdateInput
|
|
||||||
) {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
const result = await createGallery({
|
const result = await createGallery({
|
||||||
variables: {
|
variables: {
|
||||||
input: input as GQL.GalleryCreateInput,
|
input,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.galleryCreate) {
|
if (result.data?.galleryCreate) {
|
||||||
@@ -202,7 +198,10 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
} else {
|
} else {
|
||||||
const result = await updateGallery({
|
const result = await updateGallery({
|
||||||
variables: {
|
variables: {
|
||||||
input: input as GQL.GalleryUpdateInput,
|
input: {
|
||||||
|
id: gallery.id!,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.galleryUpdate) {
|
if (result.data?.galleryUpdate) {
|
||||||
@@ -216,7 +215,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
formik.resetForm({ values: formik.values });
|
formik.resetForm();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
@@ -271,7 +270,10 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentGallery = getGalleryInput(formik.values);
|
const currentGallery = {
|
||||||
|
id: gallery.id!,
|
||||||
|
...formik.values,
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<GalleryScrapeDialog
|
<GalleryScrapeDialog
|
||||||
@@ -384,7 +386,7 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId={title} as={Row}>
|
<Form.Group controlId={field} as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title,
|
title,
|
||||||
})}
|
})}
|
||||||
@@ -419,7 +421,9 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!formik.dirty}
|
disabled={
|
||||||
|
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||||
|
}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.save" />
|
<FormattedMessage id="actions.save" />
|
||||||
@@ -561,8 +565,8 @@ export const GalleryEditPanel: React.FC<IProps> = ({
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="gallery-description text-input"
|
className="gallery-description text-input"
|
||||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
onChange={(e) =>
|
||||||
formik.setFieldValue("details", newValue.currentTarget.value)
|
formik.setFieldValue("details", e.currentTarget.value)
|
||||||
}
|
}
|
||||||
value={formik.values.details}
|
value={formik.values.details}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { Prompt } from "react-router-dom";
|
|||||||
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
import { RatingSystem } from "src/components/Shared/Rating/RatingSystem";
|
||||||
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";
|
||||||
|
|
||||||
interface IProps {
|
interface IProps {
|
||||||
image: GQL.ImageDataFragment;
|
image: GQL.ImageDataFragment;
|
||||||
@@ -42,31 +43,44 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
const [updateImage] = useImageUpdate();
|
const [updateImage] = useImageUpdate();
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().optional().nullable(),
|
title: yup.string().ensure(),
|
||||||
rating100: yup.number().optional().nullable(),
|
url: yup.string().ensure(),
|
||||||
url: yup.string().optional().nullable(),
|
date: yup
|
||||||
date: yup.string().optional().nullable(),
|
.string()
|
||||||
studio_id: yup.string().optional().nullable(),
|
.ensure()
|
||||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
.test({
|
||||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
name: "date",
|
||||||
|
test: (value) => {
|
||||||
|
if (!value) return true;
|
||||||
|
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||||
|
if (Number.isNaN(Date.parse(value))) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||||
|
}),
|
||||||
|
rating100: yup.number().nullable().defined(),
|
||||||
|
studio_id: yup.string().required().nullable(),
|
||||||
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
tag_ids: yup.array(yup.string().required()).defined(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
title: image.title ?? "",
|
title: image.title ?? "",
|
||||||
rating100: image.rating100 ?? null,
|
|
||||||
url: image?.url ?? "",
|
url: image?.url ?? "",
|
||||||
date: image?.date ?? "",
|
date: image?.date ?? "",
|
||||||
studio_id: image.studio?.id,
|
rating100: image.rating100 ?? null,
|
||||||
|
studio_id: image.studio?.id ?? null,
|
||||||
performer_ids: (image.performers ?? []).map((p) => p.id),
|
performer_ids: (image.performers ?? []).map((p) => p.id),
|
||||||
tag_ids: (image.tags ?? []).map((t) => t.id),
|
tag_ids: (image.tags ?? []).map((t) => t.id),
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
onSubmit: (values) => onSave(getImageInput(values)),
|
onSubmit: (values) => onSave(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
@@ -95,19 +109,15 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
function getImageInput(input: InputValues): GQL.ImageUpdateInput {
|
async function onSave(input: InputValues) {
|
||||||
return {
|
|
||||||
id: image.id,
|
|
||||||
...input,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(input: GQL.ImageUpdateInput) {
|
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await updateImage({
|
const result = await updateImage({
|
||||||
variables: {
|
variables: {
|
||||||
input,
|
input: {
|
||||||
|
id: image.id,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.imageUpdate) {
|
if (result.data?.imageUpdate) {
|
||||||
@@ -117,7 +127,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
|
{ entity: intl.formatMessage({ id: "image" }).toLocaleLowerCase() }
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
formik.resetForm({ values: formik.values });
|
formik.resetForm();
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
@@ -127,7 +137,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId={title} as={Row}>
|
<Form.Group controlId={field} as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title,
|
title,
|
||||||
})}
|
})}
|
||||||
@@ -161,7 +171,7 @@ export const ImageEditPanel: React.FC<IProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!formik.dirty}
|
disabled={!formik.dirty || !isEqual(formik.errors, {})}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.save" />
|
<FormattedMessage id="actions.save" />
|
||||||
|
|||||||
@@ -33,12 +33,8 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Editing movie state
|
// Editing movie state
|
||||||
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
const [frontImage, setFrontImage] = useState<string | null>();
|
||||||
undefined
|
const [backImage, setBackImage] = useState<string | null>();
|
||||||
);
|
|
||||||
const [backImage, setBackImage] = useState<string | undefined | null>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const [updateMovie, { loading: updating }] = useMovieUpdate();
|
const [updateMovie, { loading: updating }] = useMovieUpdate();
|
||||||
@@ -59,26 +55,14 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding);
|
async function onSave(input: GQL.MovieCreateInput) {
|
||||||
|
|
||||||
function getMovieInput(
|
|
||||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
|
||||||
) {
|
|
||||||
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
|
||||||
...input,
|
|
||||||
id: movie.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(
|
|
||||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const result = await updateMovie({
|
const result = await updateMovie({
|
||||||
variables: {
|
variables: {
|
||||||
input: getMovieInput(input) as GQL.MovieUpdateInput,
|
input: {
|
||||||
|
id: movie.id,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.movieUpdate) {
|
if (result.data?.movieUpdate) {
|
||||||
@@ -214,7 +198,7 @@ const MoviePage: React.FC<IProps> = ({ movie }) => {
|
|||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
setFrontImage={setFrontImage}
|
setFrontImage={setFrontImage}
|
||||||
setBackImage={setBackImage}
|
setBackImage={setBackImage}
|
||||||
onImageEncoding={onImageEncoding}
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,24 +23,10 @@ const MovieCreate: React.FC = () => {
|
|||||||
|
|
||||||
const [createMovie] = useMovieCreate();
|
const [createMovie] = useMovieCreate();
|
||||||
|
|
||||||
const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding);
|
async function onSave(input: GQL.MovieCreateInput) {
|
||||||
|
|
||||||
function getMovieInput(
|
|
||||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
|
||||||
) {
|
|
||||||
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
|
||||||
...input,
|
|
||||||
};
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(
|
|
||||||
input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const result = await createMovie({
|
const result = await createMovie({
|
||||||
variables: getMovieInput(input) as GQL.MovieCreateInput,
|
variables: input,
|
||||||
});
|
});
|
||||||
if (result.data?.movieCreate?.id) {
|
if (result.data?.movieCreate?.id) {
|
||||||
history.push(`/movies/${result.data.movieCreate.id}`);
|
history.push(`/movies/${result.data.movieCreate.id}`);
|
||||||
@@ -92,7 +78,7 @@ const MovieCreate: React.FC = () => {
|
|||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
setFrontImage={setFrontImage}
|
setFrontImage={setFrontImage}
|
||||||
setBackImage={setBackImage}
|
setBackImage={setBackImage}
|
||||||
onImageEncoding={onImageEncoding}
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,17 +23,16 @@ import { Prompt } from "react-router-dom";
|
|||||||
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
import { MovieScrapeDialog } from "./MovieScrapeDialog";
|
||||||
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";
|
||||||
|
|
||||||
interface IMovieEditPanel {
|
interface IMovieEditPanel {
|
||||||
movie: Partial<GQL.MovieDataFragment>;
|
movie: Partial<GQL.MovieDataFragment>;
|
||||||
onSubmit: (
|
onSubmit: (movie: GQL.MovieCreateInput) => void;
|
||||||
movie: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput>
|
|
||||||
) => void;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
setFrontImage: (image?: string | null) => void;
|
setFrontImage: (image?: string | null) => void;
|
||||||
setBackImage: (image?: string | null) => void;
|
setBackImage: (image?: string | null) => void;
|
||||||
onImageEncoding: (loading?: boolean) => void;
|
setEncodingImage: (loading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||||
@@ -43,7 +42,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
setFrontImage,
|
setFrontImage,
|
||||||
setBackImage,
|
setBackImage,
|
||||||
onImageEncoding,
|
setEncodingImage,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
@@ -61,59 +60,51 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
aliases: yup.string().optional().nullable(),
|
aliases: yup.string().ensure(),
|
||||||
duration: yup.string().optional().nullable(),
|
duration: yup.number().nullable().defined(),
|
||||||
date: yup
|
date: yup
|
||||||
.string()
|
.string()
|
||||||
.optional()
|
.ensure()
|
||||||
.nullable()
|
.test({
|
||||||
.matches(/^\d{4}-\d{2}-\d{2}$/),
|
name: "date",
|
||||||
rating100: yup.number().optional().nullable(),
|
test: (value) => {
|
||||||
studio_id: yup.string().optional().nullable(),
|
if (!value) return true;
|
||||||
director: yup.string().optional().nullable(),
|
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||||
synopsis: yup.string().optional().nullable(),
|
if (Number.isNaN(Date.parse(value))) return false;
|
||||||
url: yup.string().optional().nullable(),
|
return true;
|
||||||
front_image: yup.string().optional().nullable(),
|
},
|
||||||
back_image: yup.string().optional().nullable(),
|
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||||
|
}),
|
||||||
|
studio_id: yup.string().required().nullable(),
|
||||||
|
director: yup.string().ensure(),
|
||||||
|
rating100: yup.number().nullable().defined(),
|
||||||
|
url: yup.string().ensure(),
|
||||||
|
synopsis: yup.string().ensure(),
|
||||||
|
front_image: yup.string().nullable().optional(),
|
||||||
|
back_image: yup.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: movie?.name,
|
name: movie?.name ?? "",
|
||||||
aliases: movie?.aliases,
|
aliases: movie?.aliases ?? "",
|
||||||
duration: movie?.duration,
|
duration: movie?.duration ?? null,
|
||||||
date: movie?.date,
|
date: movie?.date ?? "",
|
||||||
|
studio_id: movie?.studio?.id ?? null,
|
||||||
|
director: movie?.director ?? "",
|
||||||
rating100: movie?.rating100 ?? null,
|
rating100: movie?.rating100 ?? null,
|
||||||
studio_id: movie?.studio?.id,
|
url: movie?.url ?? "",
|
||||||
director: movie?.director,
|
synopsis: movie?.synopsis ?? "",
|
||||||
synopsis: movie?.synopsis,
|
|
||||||
url: movie?.url,
|
|
||||||
front_image: undefined,
|
|
||||||
back_image: undefined,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
onSubmit: (values) => onSubmit(getMovieInput(values)),
|
onSubmit: (values) => onSubmit(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
const encodingImage = ImageUtils.usePasteImage(showImageAlert);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setFrontImage(formik.values.front_image);
|
|
||||||
}, [formik.values.front_image, setFrontImage]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setBackImage(formik.values.back_image);
|
|
||||||
}, [formik.values.back_image, setBackImage]);
|
|
||||||
|
|
||||||
useEffect(
|
|
||||||
() => onImageEncoding(encodingImage),
|
|
||||||
[onImageEncoding, encodingImage]
|
|
||||||
);
|
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating100", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
@@ -138,35 +129,6 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function showImageAlert(imageData: string) {
|
|
||||||
setImageClipboard(imageData);
|
|
||||||
setIsImageAlertOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setImageFromClipboard(isFrontImage: boolean) {
|
|
||||||
if (isFrontImage) {
|
|
||||||
formik.setFieldValue("front_image", imageClipboard);
|
|
||||||
} else {
|
|
||||||
formik.setFieldValue("back_image", imageClipboard);
|
|
||||||
}
|
|
||||||
|
|
||||||
setImageClipboard(undefined);
|
|
||||||
setIsImageAlertOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getMovieInput(values: InputValues) {
|
|
||||||
const input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
|
||||||
...values,
|
|
||||||
rating100: values.rating100 ?? null,
|
|
||||||
studio_id: values.studio_id ?? null,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (movie && movie.id) {
|
|
||||||
(input as GQL.MovieUpdateInput).id = movie.id;
|
|
||||||
}
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
function updateMovieEditStateFromScraper(
|
function updateMovieEditStateFromScraper(
|
||||||
state: Partial<GQL.ScrapedMovieDataFragment>
|
state: Partial<GQL.ScrapedMovieDataFragment>
|
||||||
) {
|
) {
|
||||||
@@ -175,39 +137,42 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (state.aliases) {
|
if (state.aliases) {
|
||||||
formik.setFieldValue("aliases", state.aliases ?? undefined);
|
formik.setFieldValue("aliases", state.aliases);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.duration) {
|
if (state.duration) {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
"duration",
|
"duration",
|
||||||
DurationUtils.stringToSeconds(state.duration) ?? undefined
|
DurationUtils.stringToSeconds(state.duration)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.date) {
|
if (state.date) {
|
||||||
formik.setFieldValue("date", state.date ?? undefined);
|
formik.setFieldValue("date", state.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.studio && state.studio.stored_id) {
|
if (state.studio && state.studio.stored_id) {
|
||||||
formik.setFieldValue("studio_id", state.studio.stored_id ?? undefined);
|
formik.setFieldValue("studio_id", state.studio.stored_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.director) {
|
if (state.director) {
|
||||||
formik.setFieldValue("director", state.director ?? undefined);
|
formik.setFieldValue("director", state.director);
|
||||||
}
|
}
|
||||||
if (state.synopsis) {
|
if (state.synopsis) {
|
||||||
formik.setFieldValue("synopsis", state.synopsis ?? undefined);
|
formik.setFieldValue("synopsis", state.synopsis);
|
||||||
}
|
}
|
||||||
if (state.url) {
|
if (state.url) {
|
||||||
formik.setFieldValue("url", state.url ?? undefined);
|
formik.setFieldValue("url", state.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image;
|
if (state.front_image) {
|
||||||
formik.setFieldValue("front_image", imageStr ?? undefined);
|
// image is a base64 string
|
||||||
|
formik.setFieldValue("front_image", state.front_image);
|
||||||
const backImageStr = (state as GQL.ScrapedMovieDataFragment).back_image;
|
}
|
||||||
formik.setFieldValue("back_image", backImageStr ?? undefined);
|
if (state.back_image) {
|
||||||
|
// image is a base64 string
|
||||||
|
formik.setFieldValue("back_image", state.back_image);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapeMovieURL() {
|
async function onScrapeMovieURL() {
|
||||||
@@ -248,7 +213,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentMovie = getMovieInput(formik.values);
|
const currentMovie = {
|
||||||
|
id: movie.id!,
|
||||||
|
...formik.values,
|
||||||
|
};
|
||||||
|
|
||||||
// Get image paths for scrape gui
|
// Get image paths for scrape gui
|
||||||
currentMovie.front_image = movie?.front_image_path;
|
currentMovie.front_image = movie?.front_image_path;
|
||||||
@@ -272,16 +240,50 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
setScrapedMovie(undefined);
|
setScrapedMovie(undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encodingImage = ImageUtils.usePasteImage(showImageAlert);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFrontImage(formik.values.front_image);
|
||||||
|
}, [formik.values.front_image, setFrontImage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBackImage(formik.values.back_image);
|
||||||
|
}, [formik.values.back_image, setBackImage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEncodingImage(encodingImage);
|
||||||
|
}, [setEncodingImage, encodingImage]);
|
||||||
|
|
||||||
|
function onFrontImageLoad(imageData: string | null) {
|
||||||
|
formik.setFieldValue("front_image", imageData);
|
||||||
|
}
|
||||||
|
|
||||||
function onFrontImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onFrontImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, (data) =>
|
ImageUtils.onImageChange(event, onFrontImageLoad);
|
||||||
formik.setFieldValue("front_image", data)
|
}
|
||||||
);
|
|
||||||
|
function onBackImageLoad(imageData: string | null) {
|
||||||
|
formik.setFieldValue("back_image", imageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBackImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onBackImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, (data) =>
|
ImageUtils.onImageChange(event, onBackImageLoad);
|
||||||
formik.setFieldValue("back_image", data)
|
}
|
||||||
);
|
|
||||||
|
function showImageAlert(imageData: string) {
|
||||||
|
setImageClipboard(imageData);
|
||||||
|
setIsImageAlertOpen(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
function setImageFromClipboard(isFrontImage: boolean) {
|
||||||
|
if (isFrontImage) {
|
||||||
|
formik.setFieldValue("front_image", imageClipboard);
|
||||||
|
} else {
|
||||||
|
formik.setFieldValue("back_image", imageClipboard);
|
||||||
|
}
|
||||||
|
|
||||||
|
setImageClipboard(undefined);
|
||||||
|
setIsImageAlertOpen(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImageAlert() {
|
function renderImageAlert() {
|
||||||
@@ -325,7 +327,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
|
|
||||||
const isEditing = true;
|
const isEditing = true;
|
||||||
|
|
||||||
function renderTextField(field: string, title: string) {
|
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId={field} as={Row}>
|
<Form.Group controlId={field} as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
@@ -334,10 +336,13 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
placeholder={title}
|
placeholder={placeholder ?? title}
|
||||||
{...formik.getFieldProps(field)}
|
{...formik.getFieldProps(field)}
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
isInvalid={!!formik.getFieldMeta(field).error}
|
||||||
/>
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formik.getFieldMeta(field).error}
|
||||||
|
</Form.Control.Feedback>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
@@ -392,14 +397,18 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
<Col xs={9}>
|
<Col xs={9}>
|
||||||
<DurationInput
|
<DurationInput
|
||||||
numericValue={formik.values.duration ?? undefined}
|
numericValue={formik.values.duration ?? undefined}
|
||||||
onValueChange={(valueAsNumber: number) => {
|
onValueChange={(valueAsNumber) => {
|
||||||
formik.setFieldValue("duration", valueAsNumber);
|
formik.setFieldValue("duration", valueAsNumber ?? null);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|
||||||
{renderTextField("date", intl.formatMessage({ id: "date" }))}
|
{renderTextField(
|
||||||
|
"date",
|
||||||
|
intl.formatMessage({ id: "date" }),
|
||||||
|
"YYYY-MM-DD"
|
||||||
|
)}
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
<Form.Group controlId="studio" as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
@@ -410,7 +419,7 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
onSelect={(items) =>
|
onSelect={(items) =>
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
"studio_id",
|
"studio_id",
|
||||||
items.length > 0 ? items[0]?.id : undefined
|
items.length > 0 ? items[0]?.id : null
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||||
@@ -466,18 +475,14 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={() => formik.handleSubmit()}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={!formik.dirty}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onImageChange={onFrontImageChange}
|
onImageChange={onFrontImageChange}
|
||||||
onImageChangeURL={(i) => formik.setFieldValue("front_image", i)}
|
onImageChangeURL={onFrontImageLoad}
|
||||||
onClearImage={() => {
|
onClearImage={() => onFrontImageLoad(null)}
|
||||||
formik.setFieldValue("front_image", null);
|
|
||||||
}}
|
|
||||||
onBackImageChange={onBackImageChange}
|
onBackImageChange={onBackImageChange}
|
||||||
onBackImageChangeURL={(i) => formik.setFieldValue("back_image", i)}
|
onBackImageChangeURL={onBackImageLoad}
|
||||||
onClearBackImage={() => {
|
onClearBackImage={() => onBackImageLoad(null)}
|
||||||
formik.setFieldValue("back_image", null);
|
|
||||||
}}
|
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -227,7 +227,7 @@ export const EditPerformersDialog: React.FC<IListOperationProps> = (
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
as="select"
|
as="select"
|
||||||
className="input-control"
|
className="input-control"
|
||||||
value={genderToString(updateInput.gender ?? undefined)}
|
value={genderToString(updateInput.gender)}
|
||||||
onChange={(event) =>
|
onChange={(event) =>
|
||||||
setUpdateField({
|
setUpdateField({
|
||||||
gender: stringToGender(event.currentTarget.value),
|
gender: stringToGender(event.currentTarget.value),
|
||||||
|
|||||||
@@ -54,17 +54,17 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
const abbreviateCounter =
|
const abbreviateCounter =
|
||||||
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
|
(configuration?.ui as IUIConfig)?.abbreviateCounters ?? false;
|
||||||
|
|
||||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
|
||||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
|
const [image, setImage] = useState<string | null>();
|
||||||
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
// if undefined then get the existing image
|
// if undefined then get the existing image
|
||||||
// if null then get the default (no) image
|
// if null then get the default (no) image
|
||||||
// otherwise get the set image
|
// otherwise get the set image
|
||||||
const activeImage =
|
const activeImage =
|
||||||
imagePreview === undefined
|
image === undefined
|
||||||
? performer.image_path ?? ""
|
? performer.image_path ?? ""
|
||||||
: imagePreview ?? `${performer.image_path}&default=true`;
|
: image ?? `${performer.image_path}&default=true`;
|
||||||
const lightboxImages = useMemo(
|
const lightboxImages = useMemo(
|
||||||
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
() => [{ paths: { thumbnail: activeImage, image: activeImage } }],
|
||||||
[activeImage]
|
[activeImage]
|
||||||
@@ -91,10 +91,6 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const onImageChange = (image?: string | null) => setImagePreview(image);
|
|
||||||
|
|
||||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
|
||||||
|
|
||||||
async function onAutoTag() {
|
async function onAutoTag() {
|
||||||
try {
|
try {
|
||||||
await mutateMetadataAutoTag({ performers: [performer.id] });
|
await mutateMetadataAutoTag({ performers: [performer.id] });
|
||||||
@@ -254,11 +250,9 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isVisible={isEditing}
|
isVisible={isEditing}
|
||||||
onImageChange={onImageChange}
|
onCancelEditing={() => setIsEditing(false)}
|
||||||
onImageEncoding={onImageEncoding}
|
setImage={setImage}
|
||||||
onCancelEditing={() => {
|
setEncodingImage={setEncodingImage}
|
||||||
setIsEditing(false);
|
|
||||||
}}
|
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
@@ -393,7 +387,7 @@ const PerformerPage: React.FC<IProps> = ({ performer }) => {
|
|||||||
</Helmet>
|
</Helmet>
|
||||||
|
|
||||||
<div className="performer-image-container col-md-4 text-center">
|
<div className="performer-image-container col-md-4 text-center">
|
||||||
{imageEncoding ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
<Button variant="link" onClick={() => showLightbox()}>
|
<Button variant="link" onClick={() => showLightbox()}>
|
||||||
|
|||||||
@@ -5,8 +5,8 @@ import { PerformerEditPanel } from "./PerformerEditPanel";
|
|||||||
import { useLocation } from "react-router-dom";
|
import { useLocation } from "react-router-dom";
|
||||||
|
|
||||||
const PerformerCreate: React.FC = () => {
|
const PerformerCreate: React.FC = () => {
|
||||||
const [imagePreview, setImagePreview] = useState<string | null>();
|
const [image, setImage] = useState<string | null>();
|
||||||
const [imageEncoding, setImageEncoding] = useState<boolean>(false);
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
const query = useMemo(() => new URLSearchParams(location.search), [location]);
|
||||||
@@ -14,21 +14,17 @@ const PerformerCreate: React.FC = () => {
|
|||||||
name: query.get("q") ?? undefined,
|
name: query.get("q") ?? undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
const activeImage = imagePreview ?? "";
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const onImageChange = (image?: string | null) => setImagePreview(image);
|
|
||||||
const onImageEncoding = (isEncoding = false) => setImageEncoding(isEncoding);
|
|
||||||
|
|
||||||
function renderPerformerImage() {
|
function renderPerformerImage() {
|
||||||
if (imageEncoding) {
|
if (encodingImage) {
|
||||||
return <LoadingIndicator message="Encoding image..." />;
|
return <LoadingIndicator message="Encoding image..." />;
|
||||||
}
|
}
|
||||||
if (activeImage) {
|
if (image) {
|
||||||
return (
|
return (
|
||||||
<img
|
<img
|
||||||
className="performer"
|
className="performer"
|
||||||
src={activeImage}
|
src={image}
|
||||||
alt={intl.formatMessage({ id: "performer" })}
|
alt={intl.formatMessage({ id: "performer" })}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -50,8 +46,8 @@ const PerformerCreate: React.FC = () => {
|
|||||||
<PerformerEditPanel
|
<PerformerEditPanel
|
||||||
performer={performer}
|
performer={performer}
|
||||||
isVisible
|
isVisible
|
||||||
onImageChange={onImageChange}
|
setImage={setImage}
|
||||||
onImageEncoding={onImageEncoding}
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -27,8 +27,8 @@ import { useToast } from "src/hooks/Toast";
|
|||||||
import { Prompt, useHistory } from "react-router-dom";
|
import { Prompt, useHistory } from "react-router-dom";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
import {
|
import {
|
||||||
genderStrings,
|
|
||||||
genderToString,
|
genderToString,
|
||||||
|
stringGenderMap,
|
||||||
stringToGender,
|
stringToGender,
|
||||||
} from "src/utils/gender";
|
} from "src/utils/gender";
|
||||||
import { ConfigurationContext } from "src/hooks/Config";
|
import { ConfigurationContext } from "src/hooks/Config";
|
||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
faTrashAlt,
|
faTrashAlt,
|
||||||
} from "@fortawesome/free-solid-svg-icons";
|
} from "@fortawesome/free-solid-svg-icons";
|
||||||
import { StringListInput } from "src/components/Shared/StringListInput";
|
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||||
|
import isEqual from "lodash-es/isEqual";
|
||||||
|
|
||||||
const isScraper = (
|
const isScraper = (
|
||||||
scraper: GQL.Scraper | GQL.StashBox
|
scraper: GQL.Scraper | GQL.StashBox
|
||||||
@@ -50,24 +51,24 @@ const isScraper = (
|
|||||||
interface IPerformerDetails {
|
interface IPerformerDetails {
|
||||||
performer: Partial<GQL.PerformerDataFragment>;
|
performer: Partial<GQL.PerformerDataFragment>;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
onImageChange?: (image?: string | null) => void;
|
|
||||||
onImageEncoding?: (loading?: boolean) => void;
|
|
||||||
onCancelEditing?: () => void;
|
onCancelEditing?: () => void;
|
||||||
|
setImage: (image?: string | null) => void;
|
||||||
|
setEncodingImage: (loading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
||||||
performer,
|
performer,
|
||||||
isVisible,
|
isVisible,
|
||||||
onImageChange,
|
|
||||||
onImageEncoding,
|
|
||||||
onCancelEditing,
|
onCancelEditing,
|
||||||
|
setImage,
|
||||||
|
setEncodingImage,
|
||||||
}) => {
|
}) => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const history = useHistory();
|
const history = useHistory();
|
||||||
|
|
||||||
const isNew = performer.id === undefined;
|
const isNew = performer.id === undefined;
|
||||||
|
|
||||||
// Editing stat
|
// Editing state
|
||||||
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox>();
|
const [scraper, setScraper] = useState<GQL.Scraper | IStashBox>();
|
||||||
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>();
|
const [newTags, setNewTags] = useState<GQL.ScrapedTag[]>();
|
||||||
const [isScraperModalOpen, setIsScraperModalOpen] = useState<boolean>(false);
|
const [isScraperModalOpen, setIsScraperModalOpen] = useState<boolean>(false);
|
||||||
@@ -81,18 +82,13 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
const Scrapers = useListPerformerScrapers();
|
const Scrapers = useListPerformerScrapers();
|
||||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||||
|
|
||||||
const [scrapedPerformer, setScrapedPerformer] = useState<
|
const [scrapedPerformer, setScrapedPerformer] =
|
||||||
GQL.ScrapedPerformer | undefined
|
useState<GQL.ScrapedPerformer>();
|
||||||
>();
|
|
||||||
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
const { configuration: stashConfig } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
|
||||||
|
|
||||||
const [createTag] = useTagCreate();
|
const [createTag] = useTagCreate();
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const genderOptions = [""].concat(genderStrings);
|
|
||||||
|
|
||||||
const labelXS = 3;
|
const labelXS = 3;
|
||||||
const labelXL = 2;
|
const labelXL = 2;
|
||||||
const fieldXS = 9;
|
const fieldXS = 9;
|
||||||
@@ -100,102 +96,121 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
disambiguation: yup.string().optional(),
|
disambiguation: yup.string().ensure(),
|
||||||
alias_list: yup
|
alias_list: yup
|
||||||
.array(yup.string().required())
|
.array(yup.string().required())
|
||||||
.optional()
|
.defined()
|
||||||
.test({
|
.test({
|
||||||
name: "unique",
|
name: "unique",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
test: (value, context) => {
|
||||||
test: (value: any) => {
|
if (!value) return true;
|
||||||
return (value ?? []).length === new Set(value).size;
|
const aliases = new Set(value);
|
||||||
|
aliases.add(context.parent.name);
|
||||||
|
return value.length + 1 === aliases.size;
|
||||||
},
|
},
|
||||||
message: intl.formatMessage({ id: "dialogs.aliases_must_be_unique" }),
|
message: intl.formatMessage({
|
||||||
|
id: "validation.aliases_must_be_unique",
|
||||||
}),
|
}),
|
||||||
gender: yup.string().optional().oneOf(genderOptions),
|
}),
|
||||||
birthdate: yup.string().optional(),
|
gender: yup.string<GQL.GenderEnum | "">().ensure(),
|
||||||
ethnicity: yup.string().optional(),
|
birthdate: yup
|
||||||
eye_color: yup.string().optional(),
|
.string()
|
||||||
country: yup.string().optional(),
|
.ensure()
|
||||||
height_cm: yup.number().optional(),
|
.test({
|
||||||
measurements: yup.string().optional(),
|
name: "date",
|
||||||
fake_tits: yup.string().optional(),
|
test: (value) => {
|
||||||
career_length: yup.string().optional(),
|
if (!value) return true;
|
||||||
tattoos: yup.string().optional(),
|
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||||
piercings: yup.string().optional(),
|
if (Number.isNaN(Date.parse(value))) return false;
|
||||||
url: yup.string().optional(),
|
return true;
|
||||||
twitter: yup.string().optional(),
|
},
|
||||||
instagram: yup.string().optional(),
|
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||||
tag_ids: yup.array(yup.string().required()).optional(),
|
}),
|
||||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().optional(),
|
death_date: yup
|
||||||
image: yup.string().optional().nullable(),
|
.string()
|
||||||
details: yup.string().optional(),
|
.ensure()
|
||||||
death_date: yup.string().optional(),
|
.test({
|
||||||
hair_color: yup.string().optional(),
|
name: "date",
|
||||||
weight: yup.number().optional(),
|
test: (value) => {
|
||||||
ignore_auto_tag: yup.boolean().optional(),
|
if (!value) return true;
|
||||||
|
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||||
|
if (Number.isNaN(Date.parse(value))) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||||
|
}),
|
||||||
|
country: yup.string().ensure(),
|
||||||
|
ethnicity: yup.string().ensure(),
|
||||||
|
hair_color: yup.string().ensure(),
|
||||||
|
eye_color: yup.string().ensure(),
|
||||||
|
height_cm: yup.number().nullable().defined().default(null),
|
||||||
|
weight: yup.number().nullable().defined().default(null),
|
||||||
|
measurements: yup.string().ensure(),
|
||||||
|
fake_tits: yup.string().ensure(),
|
||||||
|
tattoos: yup.string().ensure(),
|
||||||
|
piercings: yup.string().ensure(),
|
||||||
|
career_length: yup.string().ensure(),
|
||||||
|
url: yup.string().ensure(),
|
||||||
|
twitter: yup.string().ensure(),
|
||||||
|
instagram: yup.string().ensure(),
|
||||||
|
details: yup.string().ensure(),
|
||||||
|
tag_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
ignore_auto_tag: yup.boolean().defined(),
|
||||||
|
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||||
|
image: yup.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: performer.name ?? "",
|
name: performer.name ?? "",
|
||||||
disambiguation: performer.disambiguation ?? "",
|
disambiguation: performer.disambiguation ?? "",
|
||||||
alias_list: performer.alias_list?.slice().sort(),
|
alias_list: performer.alias_list ?? [],
|
||||||
gender: genderToString(performer.gender ?? undefined),
|
gender: (performer.gender as GQL.GenderEnum) ?? "",
|
||||||
birthdate: performer.birthdate ?? "",
|
birthdate: performer.birthdate ?? "",
|
||||||
ethnicity: performer.ethnicity ?? "",
|
death_date: performer.death_date ?? "",
|
||||||
eye_color: performer.eye_color ?? "",
|
|
||||||
country: performer.country ?? "",
|
country: performer.country ?? "",
|
||||||
height_cm: performer.height_cm ?? undefined,
|
ethnicity: performer.ethnicity ?? "",
|
||||||
|
hair_color: performer.hair_color ?? "",
|
||||||
|
eye_color: performer.eye_color ?? "",
|
||||||
|
height_cm: performer.height_cm ?? null,
|
||||||
|
weight: performer.weight ?? null,
|
||||||
measurements: performer.measurements ?? "",
|
measurements: performer.measurements ?? "",
|
||||||
fake_tits: performer.fake_tits ?? "",
|
fake_tits: performer.fake_tits ?? "",
|
||||||
career_length: performer.career_length ?? "",
|
|
||||||
tattoos: performer.tattoos ?? "",
|
tattoos: performer.tattoos ?? "",
|
||||||
piercings: performer.piercings ?? "",
|
piercings: performer.piercings ?? "",
|
||||||
|
career_length: performer.career_length ?? "",
|
||||||
url: performer.url ?? "",
|
url: performer.url ?? "",
|
||||||
twitter: performer.twitter ?? "",
|
twitter: performer.twitter ?? "",
|
||||||
instagram: performer.instagram ?? "",
|
instagram: performer.instagram ?? "",
|
||||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
|
||||||
stash_ids: performer.stash_ids ?? undefined,
|
|
||||||
image: undefined,
|
|
||||||
details: performer.details ?? "",
|
details: performer.details ?? "",
|
||||||
death_date: performer.death_date ?? "",
|
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||||
hair_color: performer.hair_color ?? "",
|
|
||||||
weight: performer.weight ?? undefined,
|
|
||||||
ignore_auto_tag: performer.ignore_auto_tag ?? false,
|
ignore_auto_tag: performer.ignore_auto_tag ?? false,
|
||||||
|
stash_ids: getStashIDs(performer.stash_ids),
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
onSubmit: (values) => onSave(values),
|
onSubmit: (values) => onSave(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
// always dirty if creating a new performer with a name
|
|
||||||
if (isNew && performer.name) {
|
|
||||||
formik.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function translateScrapedGender(scrapedGender?: string) {
|
function translateScrapedGender(scrapedGender?: string) {
|
||||||
if (!scrapedGender) {
|
if (!scrapedGender) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let retEnum: GQL.GenderEnum | undefined;
|
|
||||||
|
|
||||||
// try to translate from enum values first
|
// try to translate from enum values first
|
||||||
const upperGender = scrapedGender?.toUpperCase();
|
const upperGender = scrapedGender.toUpperCase();
|
||||||
const asEnum = genderToString(upperGender);
|
const asEnum = genderToString(upperGender);
|
||||||
if (asEnum) {
|
if (asEnum) {
|
||||||
retEnum = stringToGender(asEnum);
|
return stringToGender(asEnum);
|
||||||
} else {
|
} else {
|
||||||
// try to match against gender strings
|
// try to match against gender strings
|
||||||
const caseInsensitive = true;
|
const caseInsensitive = true;
|
||||||
retEnum = stringToGender(scrapedGender, caseInsensitive);
|
return stringToGender(scrapedGender, caseInsensitive);
|
||||||
}
|
}
|
||||||
|
|
||||||
return genderToString(retEnum);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderNewTags() {
|
function renderNewTags() {
|
||||||
@@ -329,10 +344,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
}
|
}
|
||||||
if (state.gender) {
|
if (state.gender) {
|
||||||
// gender is a string in the scraper data
|
// gender is a string in the scraper data
|
||||||
formik.setFieldValue(
|
const newGender = translateScrapedGender(state.gender);
|
||||||
"gender",
|
if (newGender) {
|
||||||
translateScrapedGender(state.gender ?? undefined)
|
formik.setFieldValue("gender", newGender);
|
||||||
);
|
}
|
||||||
}
|
}
|
||||||
if (state.tags) {
|
if (state.tags) {
|
||||||
// map tags to their ids and filter out those not found
|
// map tags to their ids and filter out those not found
|
||||||
@@ -345,15 +360,14 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
// image is a base64 string
|
// image is a base64 string
|
||||||
// #404: don't overwrite image if it has been modified by the user
|
// #404: don't overwrite image if it has been modified by the user
|
||||||
// overwrite if not new since it came from a dialog
|
// overwrite if not new since it came from a dialog
|
||||||
// overwrite if image was cleared (`null`)
|
// overwrite if image is unset
|
||||||
// otherwise follow existing behaviour (`undefined`)
|
|
||||||
if (
|
if (
|
||||||
(!isNew || [null, undefined].includes(formik.values.image)) &&
|
(!isNew || !formik.values.image) &&
|
||||||
state.images &&
|
state.images &&
|
||||||
state.images.length > 0
|
state.images.length > 0
|
||||||
) {
|
) {
|
||||||
const imageStr = state.images[0];
|
const imageStr = state.images[0];
|
||||||
formik.setFieldValue("image", imageStr ?? undefined);
|
formik.setFieldValue("image", imageStr);
|
||||||
}
|
}
|
||||||
if (state.details) {
|
if (state.details) {
|
||||||
formik.setFieldValue("details", state.details);
|
formik.setFieldValue("details", state.details);
|
||||||
@@ -382,31 +396,47 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImage(formik.values.image);
|
||||||
|
}, [formik.values.image, setImage]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => setEncodingImage(encodingImage),
|
||||||
|
[setEncodingImage, encodingImage]
|
||||||
|
);
|
||||||
|
|
||||||
|
function onImageLoad(imageData: string | null) {
|
||||||
formik.setFieldValue("image", imageData);
|
formik.setFieldValue("image", imageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave(performerInput: InputValues) {
|
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function onSave(input: InputValues) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (isNew) {
|
if (isNew) {
|
||||||
const input = getCreateValues(performerInput);
|
|
||||||
const result = await createPerformer({
|
const result = await createPerformer({
|
||||||
variables: {
|
variables: {
|
||||||
input,
|
input: {
|
||||||
|
...input,
|
||||||
|
gender: input.gender || null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.performerCreate) {
|
if (result.data?.performerCreate) {
|
||||||
history.push(`/performers/${result.data.performerCreate.id}`);
|
history.push(`/performers/${result.data.performerCreate.id}`);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const input = getUpdateValues(performerInput);
|
|
||||||
|
|
||||||
await updatePerformer({
|
await updatePerformer({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
|
id: performer.id!,
|
||||||
...input,
|
...input,
|
||||||
stash_ids: getStashIDs(performerInput?.stash_ids),
|
gender: input.gender || null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -440,16 +470,12 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (onImageChange) {
|
setImage(formik.values.image);
|
||||||
onImageChange(formik.values.image);
|
}, [formik.values.image, setImage]);
|
||||||
}
|
|
||||||
return () => onImageChange?.();
|
|
||||||
}, [formik.values.image, onImageChange]);
|
|
||||||
|
|
||||||
useEffect(
|
useEffect(() => {
|
||||||
() => onImageEncoding?.(imageEncoding),
|
setEncodingImage(encodingImage);
|
||||||
[onImageEncoding, imageEncoding]
|
}, [setEncodingImage, encodingImage]);
|
||||||
);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const newQueryableScrapers = (
|
const newQueryableScrapers = (
|
||||||
@@ -463,33 +489,6 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
function getUpdateValues(values: InputValues): GQL.PerformerUpdateInput {
|
|
||||||
return {
|
|
||||||
...values,
|
|
||||||
gender: stringToGender(values.gender) ?? null,
|
|
||||||
height_cm: values.height_cm ? Number(values.height_cm) : null,
|
|
||||||
weight: values.weight ? Number(values.weight) : null,
|
|
||||||
id: performer.id ?? "",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function getCreateValues(values: InputValues): GQL.PerformerCreateInput {
|
|
||||||
return {
|
|
||||||
...values,
|
|
||||||
gender: stringToGender(values.gender),
|
|
||||||
height_cm: values.height_cm ? Number(values.height_cm) : null,
|
|
||||||
weight: values.weight ? Number(values.weight) : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onImageChangeURL(url: string) {
|
|
||||||
formik.setFieldValue("image", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onReloadScrapers() {
|
async function onReloadScrapers() {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
@@ -655,9 +654,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentPerformer: Partial<GQL.PerformerUpdateInput> = {
|
const currentPerformer = {
|
||||||
...formik.values,
|
...formik.values,
|
||||||
gender: stringToGender(formik.values.gender),
|
gender: formik.values.gender || null,
|
||||||
image: formik.values.image ?? performer.image_path,
|
image: formik.values.image ?? performer.image_path,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -698,8 +697,8 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{renderScraperMenu()}
|
{renderScraperMenu()}
|
||||||
<ImageInput
|
<ImageInput
|
||||||
isEditing
|
isEditing
|
||||||
onImageChange={onImageChangeHandler}
|
onImageChange={onImageChange}
|
||||||
onImageURL={onImageChangeURL}
|
onImageURL={onImageLoad}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
@@ -712,7 +711,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
variant="success"
|
variant="success"
|
||||||
disabled={!formik.dirty}
|
disabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.save" />
|
<FormattedMessage id="actions.save" />
|
||||||
@@ -843,6 +842,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
{...formik.getFieldProps(field)}
|
{...formik.getFieldProps(field)}
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
isInvalid={!!formik.getFieldMeta(field).error}
|
||||||
/>
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formik.getFieldMeta(field).error}
|
||||||
|
</Form.Control.Feedback>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
@@ -902,7 +904,11 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
<StringListInput
|
<StringListInput
|
||||||
value={formik.values.alias_list ?? []}
|
value={formik.values.alias_list ?? []}
|
||||||
setValue={(value) => formik.setFieldValue("alias_list", value)}
|
setValue={(value) => formik.setFieldValue("alias_list", value)}
|
||||||
errors={formik.errors.alias_list}
|
errors={
|
||||||
|
Array.isArray(formik.errors.alias_list)
|
||||||
|
? formik.errors.alias_list[0]
|
||||||
|
: formik.errors.alias_list
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -917,9 +923,10 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
className="input-control"
|
className="input-control"
|
||||||
{...formik.getFieldProps("gender")}
|
{...formik.getFieldProps("gender")}
|
||||||
>
|
>
|
||||||
{genderOptions.map((opt) => (
|
<option value="" key=""></option>
|
||||||
<option value={opt} key={opt}>
|
{Array.from(stringGenderMap.entries()).map(([name, value]) => (
|
||||||
{opt}
|
<option value={value} key={value}>
|
||||||
|
{name}
|
||||||
</option>
|
</option>
|
||||||
))}
|
))}
|
||||||
</Form.Control>
|
</Form.Control>
|
||||||
|
|||||||
@@ -152,8 +152,8 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
let retEnum: GQL.GenderEnum | undefined;
|
let retEnum: GQL.GenderEnum | undefined;
|
||||||
|
|
||||||
// try to translate from enum values first
|
// try to translate from enum values first
|
||||||
const upperGender = scrapedGender?.toUpperCase();
|
const upperGender = scrapedGender.toUpperCase();
|
||||||
const asEnum = genderToString(upperGender as GQL.GenderEnum);
|
const asEnum = genderToString(upperGender);
|
||||||
if (asEnum) {
|
if (asEnum) {
|
||||||
retEnum = stringToGender(asEnum);
|
retEnum = stringToGender(asEnum);
|
||||||
} else {
|
} else {
|
||||||
@@ -248,7 +248,7 @@ export const PerformerScrapeDialog: React.FC<IPerformerScrapeDialogProps> = (
|
|||||||
);
|
);
|
||||||
const [gender, setGender] = useState<ScrapeResult<string>>(
|
const [gender, setGender] = useState<ScrapeResult<string>>(
|
||||||
new ScrapeResult<string>(
|
new ScrapeResult<string>(
|
||||||
genderToString(props.performer.gender ?? undefined),
|
genderToString(props.performer.gender),
|
||||||
translateScrapedGender(props.scraped.gender)
|
translateScrapedGender(props.scraped.gender)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -51,6 +51,7 @@ import { objectTitle } from "src/core/files";
|
|||||||
import { galleryTitle } from "src/core/galleries";
|
import { galleryTitle } from "src/core/galleries";
|
||||||
import { useRatingKeybinds } from "src/hooks/keybinds";
|
import { useRatingKeybinds } from "src/hooks/keybinds";
|
||||||
import { lazyComponent } from "src/utils/lazyComponent";
|
import { lazyComponent } from "src/utils/lazyComponent";
|
||||||
|
import isEqual from "lodash-es/isEqual";
|
||||||
|
|
||||||
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
const SceneScrapeDialog = lazyComponent(() => import("./SceneScrapeDialog"));
|
||||||
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
const SceneQueryModal = lazyComponent(() => import("./SceneQueryModal"));
|
||||||
@@ -115,55 +116,66 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
const [updateScene] = useSceneUpdate();
|
const [updateScene] = useSceneUpdate();
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
title: yup.string().optional().nullable(),
|
title: yup.string().ensure(),
|
||||||
code: yup.string().optional().nullable(),
|
code: yup.string().ensure(),
|
||||||
details: yup.string().optional().nullable(),
|
url: yup.string().ensure(),
|
||||||
director: yup.string().optional().nullable(),
|
date: yup
|
||||||
url: yup.string().optional().nullable(),
|
.string()
|
||||||
date: yup.string().optional().nullable(),
|
.ensure()
|
||||||
rating100: yup.number().optional().nullable(),
|
.test({
|
||||||
gallery_ids: yup.array(yup.string().required()).optional().nullable(),
|
name: "date",
|
||||||
studio_id: yup.string().optional().nullable(),
|
test: (value) => {
|
||||||
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
if (!value) return true;
|
||||||
|
if (!value.match(/^\d{4}-\d{2}-\d{2}$/)) return false;
|
||||||
|
if (Number.isNaN(Date.parse(value))) return false;
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
message: intl.formatMessage({ id: "validation.date_invalid_form" }),
|
||||||
|
}),
|
||||||
|
director: yup.string().ensure(),
|
||||||
|
rating100: yup.number().nullable().defined(),
|
||||||
|
gallery_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
studio_id: yup.string().required().nullable(),
|
||||||
|
performer_ids: yup.array(yup.string().required()).defined(),
|
||||||
movies: yup
|
movies: yup
|
||||||
.array(
|
.array(
|
||||||
yup.object({
|
yup.object({
|
||||||
movie_id: yup.string().required(),
|
movie_id: yup.string().required(),
|
||||||
scene_index: yup.string().optional().nullable(),
|
scene_index: yup.number().nullable().defined(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.optional()
|
.defined(),
|
||||||
.nullable(),
|
tag_ids: yup.array(yup.string().required()).defined(),
|
||||||
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||||
cover_image: yup.string().optional().nullable(),
|
details: yup.string().ensure(),
|
||||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().optional().nullable(),
|
cover_image: yup.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = useMemo(
|
const initialValues = useMemo(
|
||||||
() => ({
|
() => ({
|
||||||
title: scene.title ?? "",
|
title: scene.title ?? "",
|
||||||
code: scene.code ?? "",
|
code: scene.code ?? "",
|
||||||
details: scene.details ?? "",
|
|
||||||
director: scene.director ?? "",
|
|
||||||
url: scene.url ?? "",
|
url: scene.url ?? "",
|
||||||
date: scene.date ?? "",
|
date: scene.date ?? "",
|
||||||
|
director: scene.director ?? "",
|
||||||
rating100: scene.rating100 ?? null,
|
rating100: scene.rating100 ?? null,
|
||||||
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
||||||
studio_id: scene.studio?.id,
|
studio_id: scene.studio?.id ?? null,
|
||||||
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
||||||
movies: (scene.movies ?? []).map((m) => {
|
movies: (scene.movies ?? []).map((m) => {
|
||||||
return { movie_id: m.movie.id, scene_index: m.scene_index };
|
return { movie_id: m.movie.id, scene_index: m.scene_index ?? null };
|
||||||
}),
|
}),
|
||||||
tag_ids: (scene.tags ?? []).map((t) => t.id),
|
tag_ids: (scene.tags ?? []).map((t) => t.id),
|
||||||
cover_image: initialCoverImage,
|
|
||||||
stash_ids: getStashIDs(scene.stash_ids),
|
stash_ids: getStashIDs(scene.stash_ids),
|
||||||
|
details: scene.details ?? "",
|
||||||
|
cover_image: initialCoverImage,
|
||||||
}),
|
}),
|
||||||
[scene, initialCoverImage]
|
[scene, initialCoverImage]
|
||||||
);
|
);
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
@@ -225,15 +237,6 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
setQueryableScrapers(newQueryableScrapers);
|
setQueryableScrapers(newQueryableScrapers);
|
||||||
}, [Scrapers, stashConfig]);
|
}, [Scrapers, stashConfig]);
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
|
||||||
|
|
||||||
function getSceneInput(input: InputValues): GQL.SceneUpdateInput {
|
|
||||||
return {
|
|
||||||
id: scene.id!,
|
|
||||||
...input,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function setMovieIds(movieIds: string[]) {
|
function setMovieIds(movieIds: string[]) {
|
||||||
const existingMovies = formik.values.movies;
|
const existingMovies = formik.values.movies;
|
||||||
|
|
||||||
@@ -245,29 +248,23 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
movie_id: m,
|
movie_id: m,
|
||||||
|
scene_index: null,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
formik.setFieldValue("movies", newMovies);
|
formik.setFieldValue("movies", newMovies);
|
||||||
}
|
}
|
||||||
|
|
||||||
function getCreateValues(values: InputValues): GQL.SceneCreateInput {
|
|
||||||
return {
|
|
||||||
...values,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(input: InputValues) {
|
async function onSave(input: InputValues) {
|
||||||
|
console.log("onSave", input);
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
const updateValues = getSceneInput(input);
|
|
||||||
const result = await updateScene({
|
const result = await updateScene({
|
||||||
variables: {
|
variables: {
|
||||||
input: {
|
input: {
|
||||||
...updateValues,
|
|
||||||
id: scene.id!,
|
id: scene.id!,
|
||||||
rating100: input.rating100 ?? null,
|
...input,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -280,20 +277,17 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
),
|
),
|
||||||
});
|
});
|
||||||
|
formik.resetForm();
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const createValues = getCreateValues(input);
|
|
||||||
const result = await mutateCreateScene({
|
const result = await mutateCreateScene({
|
||||||
...createValues,
|
...input,
|
||||||
file_ids: fileID ? [fileID] : undefined,
|
file_ids: fileID ? [fileID] : undefined,
|
||||||
});
|
});
|
||||||
if (result.data?.sceneCreate?.id) {
|
if (result.data?.sceneCreate?.id) {
|
||||||
history.push(`/scenes/${result.data?.sceneCreate.id}`);
|
history.push(`/scenes/${result.data?.sceneCreate.id}`);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// clear the cover image so that it doesn't appear dirty
|
|
||||||
formik.resetForm({ values: formik.values });
|
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
Toast.error(e);
|
Toast.error(e);
|
||||||
}
|
}
|
||||||
@@ -321,6 +315,8 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
function onImageLoad(imageData: string) {
|
||||||
setCoverImagePreview(imageData);
|
setCoverImagePreview(imageData);
|
||||||
formik.setFieldValue("cover_image", imageData);
|
formik.setFieldValue("cover_image", imageData);
|
||||||
@@ -356,17 +352,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
) {
|
) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const input: GQL.ScrapedSceneInput = {
|
const result = await queryScrapeSceneQueryFragment(s, fragment);
|
||||||
date: fragment.date,
|
|
||||||
code: fragment.code,
|
|
||||||
details: fragment.details,
|
|
||||||
director: fragment.director,
|
|
||||||
remote_site_id: fragment.remote_site_id,
|
|
||||||
title: fragment.title,
|
|
||||||
url: fragment.url,
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await queryScrapeSceneQueryFragment(s, input);
|
|
||||||
if (!result.data || !result.data.scrapeSingleScene?.length) {
|
if (!result.data || !result.data.scrapeSingleScene?.length) {
|
||||||
Toast.success({
|
Toast.success({
|
||||||
content: "No scenes found",
|
content: "No scenes found",
|
||||||
@@ -414,7 +400,11 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentScene = getSceneInput(formik.values);
|
const currentScene = {
|
||||||
|
id: scene.id!,
|
||||||
|
...formik.values,
|
||||||
|
};
|
||||||
|
|
||||||
if (!currentScene.cover_image) {
|
if (!currentScene.cover_image) {
|
||||||
currentScene.cover_image = scene.paths?.screenshot;
|
currentScene.cover_image = scene.paths?.screenshot;
|
||||||
}
|
}
|
||||||
@@ -671,7 +661,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
function renderTextField(field: string, title: string, placeholder?: string) {
|
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||||
return (
|
return (
|
||||||
<Form.Group controlId={title} as={Row}>
|
<Form.Group controlId={field} as={Row}>
|
||||||
{FormUtils.renderLabel({
|
{FormUtils.renderLabel({
|
||||||
title,
|
title,
|
||||||
})}
|
})}
|
||||||
@@ -682,13 +672,16 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
{...formik.getFieldProps(field)}
|
{...formik.getFieldProps(field)}
|
||||||
isInvalid={!!formik.getFieldMeta(field).error}
|
isInvalid={!!formik.getFieldMeta(field).error}
|
||||||
/>
|
/>
|
||||||
|
<Form.Control.Feedback type="invalid">
|
||||||
|
{formik.getFieldMeta(field).error}
|
||||||
|
</Form.Control.Feedback>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
const image = useMemo(() => {
|
const image = useMemo(() => {
|
||||||
if (imageEncoding) {
|
if (encodingImage) {
|
||||||
return <LoadingIndicator message="Encoding image..." />;
|
return <LoadingIndicator message="Encoding image..." />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -703,7 +696,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
return <div></div>;
|
return <div></div>;
|
||||||
}, [imageEncoding, coverImagePreview, intl]);
|
}, [encodingImage, coverImagePreview, intl]);
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
@@ -722,7 +715,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
<Button
|
<Button
|
||||||
className="edit-button"
|
className="edit-button"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
disabled={!isNew && !formik.dirty}
|
disabled={
|
||||||
|
(!isNew && !formik.dirty) || !isEqual(formik.errors, {})
|
||||||
|
}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
<FormattedMessage id="actions.save" />
|
<FormattedMessage id="actions.save" />
|
||||||
@@ -946,10 +941,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
<Form.Control
|
<Form.Control
|
||||||
as="textarea"
|
as="textarea"
|
||||||
className="scene-description text-input"
|
className="scene-description text-input"
|
||||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
onChange={(e) =>
|
||||||
formik.setFieldValue("details", newValue.currentTarget.value)
|
formik.setFieldValue("details", e.currentTarget.value)
|
||||||
}
|
}
|
||||||
value={formik.values.details}
|
value={formik.values.details ?? ""}
|
||||||
/>
|
/>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
useStudioDestroy,
|
useStudioDestroy,
|
||||||
mutateMetadataAutoTag,
|
mutateMetadataAutoTag,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import ImageUtils from "src/utils/image";
|
|
||||||
import { Counter } from "src/components/Shared/Counter";
|
import { Counter } from "src/components/Shared/Counter";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { ModalComponent } from "src/components/Shared/Modal";
|
import { ModalComponent } from "src/components/Shared/Modal";
|
||||||
@@ -54,8 +53,9 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
const [isEditing, setIsEditing] = useState<boolean>(false);
|
const [isEditing, setIsEditing] = useState<boolean>(false);
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
// Studio state
|
// Editing studio state
|
||||||
const [image, setImage] = useState<string | null>();
|
const [image, setImage] = useState<string | null>();
|
||||||
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const [updateStudio] = useStudioUpdate();
|
const [updateStudio] = useStudioUpdate();
|
||||||
const [deleteStudio] = useStudioDestroy({ id: studio.id });
|
const [deleteStudio] = useStudioDestroy({ id: studio.id });
|
||||||
@@ -73,17 +73,14 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
async function onSave(input: GQL.StudioCreateInput) {
|
||||||
setImage(imageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
|
||||||
|
|
||||||
async function onSave(input: Partial<GQL.StudioUpdateInput>) {
|
|
||||||
try {
|
try {
|
||||||
const result = await updateStudio({
|
const result = await updateStudio({
|
||||||
variables: {
|
variables: {
|
||||||
input: input as GQL.StudioUpdateInput,
|
input: {
|
||||||
|
id: studio.id,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.studioUpdate) {
|
if (result.data?.studioUpdate) {
|
||||||
@@ -181,7 +178,7 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="studio-details col-md-4">
|
<div className="studio-details col-md-4">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{imageEncoding ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
@@ -213,7 +210,8 @@ const StudioPage: React.FC<IProps> = ({ studio }) => {
|
|||||||
onSubmit={onSave}
|
onSubmit={onSave}
|
||||||
onCancel={onToggleEdit}
|
onCancel={onToggleEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
onImageChange={setImage}
|
setImage={setImage}
|
||||||
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { useIntl } from "react-intl";
|
|||||||
|
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
import { useStudioCreate } from "src/core/StashService";
|
import { useStudioCreate } from "src/core/StashService";
|
||||||
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";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { StudioEditPanel } from "./StudioEditPanel";
|
import { StudioEditPanel } from "./StudioEditPanel";
|
||||||
@@ -21,25 +20,16 @@ const StudioCreate: React.FC = () => {
|
|||||||
|
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
// Studio state
|
// Editing studio state
|
||||||
const [image, setImage] = useState<string | null>();
|
const [image, setImage] = useState<string | null>();
|
||||||
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const [createStudio] = useStudioCreate();
|
const [createStudio] = useStudioCreate();
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
async function onSave(input: GQL.StudioCreateInput) {
|
||||||
setImage(imageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
|
||||||
|
|
||||||
async function onSave(
|
|
||||||
input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const result = await createStudio({
|
const result = await createStudio({
|
||||||
variables: {
|
variables: { input },
|
||||||
input: input as GQL.StudioCreateInput,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (result.data?.studioCreate?.id) {
|
if (result.data?.studioCreate?.id) {
|
||||||
history.push(`/studios/${result.data.studioCreate.id}`);
|
history.push(`/studios/${result.data.studioCreate.id}`);
|
||||||
@@ -65,7 +55,7 @@ const StudioCreate: React.FC = () => {
|
|||||||
)}
|
)}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
{imageEncoding ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
@@ -74,9 +64,10 @@ const StudioCreate: React.FC = () => {
|
|||||||
<StudioEditPanel
|
<StudioEditPanel
|
||||||
studio={studio}
|
studio={studio}
|
||||||
onSubmit={onSave}
|
onSubmit={onSave}
|
||||||
onImageChange={setImage}
|
|
||||||
onCancel={() => history.push("/studios")}
|
onCancel={() => history.push("/studios")}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
|
setImage={setImage}
|
||||||
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,16 +17,15 @@ import { StringListInput } from "../../Shared/StringListInput";
|
|||||||
import { faTrashAlt } from "@fortawesome/free-solid-svg-icons";
|
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";
|
||||||
|
|
||||||
interface IStudioEditPanel {
|
interface IStudioEditPanel {
|
||||||
studio: Partial<GQL.StudioDataFragment>;
|
studio: Partial<GQL.StudioDataFragment>;
|
||||||
onSubmit: (
|
onSubmit: (studio: GQL.StudioCreateInput) => void;
|
||||||
studio: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput>
|
|
||||||
) => void;
|
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onImageChange?: (image?: string | null) => void;
|
setImage: (image?: string | null) => void;
|
||||||
onImageEncoding?: (loading?: boolean) => void;
|
setEncodingImage: (loading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
||||||
@@ -34,83 +33,77 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
onSubmit,
|
onSubmit,
|
||||||
onCancel,
|
onCancel,
|
||||||
onDelete,
|
onDelete,
|
||||||
onImageChange,
|
setImage,
|
||||||
onImageEncoding,
|
setEncodingImage,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
const isNew = studio.id === undefined;
|
const isNew = studio.id === undefined;
|
||||||
const { configuration } = React.useContext(ConfigurationContext);
|
const { configuration } = React.useContext(ConfigurationContext);
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
|
||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
url: yup.string().optional().nullable(),
|
url: yup.string().ensure(),
|
||||||
details: yup.string().optional().nullable(),
|
details: yup.string().ensure(),
|
||||||
image: yup.string().optional().nullable(),
|
parent_id: yup.string().required().nullable(),
|
||||||
rating100: yup.number().optional().nullable(),
|
rating100: yup.number().nullable().defined(),
|
||||||
parent_id: yup.string().optional().nullable(),
|
|
||||||
stash_ids: yup.mixed<GQL.StashIdInput[]>().optional().nullable(),
|
|
||||||
aliases: yup
|
aliases: yup
|
||||||
.array(yup.string().required())
|
.array(yup.string().required())
|
||||||
.optional()
|
.defined()
|
||||||
.test({
|
.test({
|
||||||
name: "unique",
|
name: "unique",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
test: (value, context) => {
|
||||||
test: (value: any) => {
|
if (!value) return true;
|
||||||
return (value ?? []).length === new Set(value).size;
|
const aliases = new Set(value);
|
||||||
|
aliases.add(context.parent.name);
|
||||||
|
return value.length + 1 === aliases.size;
|
||||||
},
|
},
|
||||||
message: "aliases must be unique",
|
message: intl.formatMessage({
|
||||||
|
id: "validation.aliases_must_be_unique",
|
||||||
}),
|
}),
|
||||||
ignore_auto_tag: yup.boolean().optional(),
|
}),
|
||||||
|
ignore_auto_tag: yup.boolean().defined(),
|
||||||
|
stash_ids: yup.mixed<GQL.StashIdInput[]>().defined(),
|
||||||
|
image: yup.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: studio.name ?? "",
|
name: studio.name ?? "",
|
||||||
url: studio.url ?? "",
|
url: studio.url ?? "",
|
||||||
details: studio.details ?? "",
|
details: studio.details ?? "",
|
||||||
image: undefined,
|
parent_id: studio.parent_studio?.id ?? null,
|
||||||
rating100: studio.rating100 ?? null,
|
rating100: studio.rating100 ?? null,
|
||||||
parent_id: studio.parent_studio?.id,
|
aliases: studio.aliases ?? [],
|
||||||
stash_ids: studio.stash_ids ?? undefined,
|
|
||||||
aliases: studio.aliases,
|
|
||||||
ignore_auto_tag: studio.ignore_auto_tag ?? false,
|
ignore_auto_tag: studio.ignore_auto_tag ?? false,
|
||||||
|
stash_ids: getStashIDs(studio.stash_ids),
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
|
enableReinitialize: true,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
onSubmit: (values) => onSubmit(getStudioInput(values)),
|
onSubmit: (values) => onSubmit(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
// always dirty if creating a new studio with a name
|
const encodingImage = ImageUtils.usePasteImage((imageData) =>
|
||||||
if (isNew && studio.name) {
|
formik.setFieldValue("image", imageData)
|
||||||
formik.dirty = true;
|
);
|
||||||
}
|
|
||||||
|
useEffect(() => {
|
||||||
|
setImage(formik.values.image);
|
||||||
|
}, [formik.values.image, setImage]);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
() => setEncodingImage(encodingImage),
|
||||||
|
[setEncodingImage, encodingImage]
|
||||||
|
);
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating100", v);
|
formik.setFieldValue("rating100", v);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
|
||||||
formik.setFieldValue("image", imageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getStudioInput(values: InputValues) {
|
|
||||||
const input: Partial<GQL.StudioCreateInput | GQL.StudioUpdateInput> = {
|
|
||||||
...values,
|
|
||||||
stash_ids: getStashIDs(values.stash_ids),
|
|
||||||
};
|
|
||||||
|
|
||||||
if (studio && studio.id) {
|
|
||||||
(input as GQL.StudioUpdateInput).id = studio.id;
|
|
||||||
}
|
|
||||||
return input;
|
|
||||||
}
|
|
||||||
|
|
||||||
useRatingKeybinds(
|
useRatingKeybinds(
|
||||||
true,
|
true,
|
||||||
configuration?.ui?.ratingSystemOptions?.type,
|
configuration?.ui?.ratingSystemOptions?.type,
|
||||||
@@ -126,26 +119,14 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
function onImageLoad(imageData: string | null) {
|
||||||
if (onImageChange) {
|
formik.setFieldValue("image", imageData);
|
||||||
onImageChange(formik.values.image);
|
|
||||||
}
|
}
|
||||||
return () => onImageChange?.();
|
|
||||||
}, [formik.values.image, onImageChange]);
|
|
||||||
|
|
||||||
useEffect(
|
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
() => onImageEncoding?.(imageEncoding),
|
|
||||||
[onImageEncoding, imageEncoding]
|
|
||||||
);
|
|
||||||
|
|
||||||
function onImageChangeHandler(event: React.FormEvent<HTMLInputElement>) {
|
|
||||||
ImageUtils.onImageChange(event, onImageLoad);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageChangeURL(url: string) {
|
|
||||||
formik.setFieldValue("image", url);
|
|
||||||
}
|
|
||||||
|
|
||||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||||
formik.setFieldValue(
|
formik.setFieldValue(
|
||||||
"stash_ids",
|
"stash_ids",
|
||||||
@@ -306,7 +287,11 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
<StringListInput
|
<StringListInput
|
||||||
value={formik.values.aliases ?? []}
|
value={formik.values.aliases ?? []}
|
||||||
setValue={(value) => formik.setFieldValue("aliases", value)}
|
setValue={(value) => formik.setFieldValue("aliases", value)}
|
||||||
errors={formik.errors.aliases}
|
errors={
|
||||||
|
Array.isArray(formik.errors.aliases)
|
||||||
|
? formik.errors.aliases[0]
|
||||||
|
: formik.errors.aliases
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -333,13 +318,11 @@ export const StudioEditPanel: React.FC<IStudioEditPanel> = ({
|
|||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing
|
isEditing
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={() => formik.handleSubmit()}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={!formik.dirty}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onImageChange={onImageChangeHandler}
|
onImageChange={onImageChange}
|
||||||
onImageChangeURL={onImageChangeURL}
|
onImageChangeURL={onImageLoad}
|
||||||
onClearImage={() => {
|
onClearImage={() => onImageLoad(null)}
|
||||||
formik.setFieldValue("image", null);
|
|
||||||
}}
|
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
acceptSVG
|
acceptSVG
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import {
|
|||||||
useTagDestroy,
|
useTagDestroy,
|
||||||
mutateMetadataAutoTag,
|
mutateMetadataAutoTag,
|
||||||
} from "src/core/StashService";
|
} from "src/core/StashService";
|
||||||
import ImageUtils from "src/utils/image";
|
|
||||||
import { Counter } from "src/components/Shared/Counter";
|
import { Counter } from "src/components/Shared/Counter";
|
||||||
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
import { DetailsEditNavbar } from "src/components/Shared/DetailsEditNavbar";
|
||||||
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
import { ErrorMessage } from "src/components/Shared/ErrorMessage";
|
||||||
@@ -64,6 +63,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
|
|
||||||
// Editing tag state
|
// Editing tag state
|
||||||
const [image, setImage] = useState<string | null>();
|
const [image, setImage] = useState<string | null>();
|
||||||
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const [updateTag] = useTagUpdate();
|
const [updateTag] = useTagUpdate();
|
||||||
const [deleteTag] = useTagDestroy({ id: tag.id });
|
const [deleteTag] = useTagDestroy({ id: tag.id });
|
||||||
@@ -99,27 +99,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
async function onSave(input: GQL.TagCreateInput) {
|
||||||
setImage(imageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, isEditing);
|
|
||||||
|
|
||||||
function getTagInput(
|
|
||||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
|
||||||
) {
|
|
||||||
const ret: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
|
||||||
...input,
|
|
||||||
image,
|
|
||||||
id: tag.id,
|
|
||||||
};
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(
|
|
||||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const oldRelations = {
|
const oldRelations = {
|
||||||
parents: tag.parents ?? [],
|
parents: tag.parents ?? [],
|
||||||
@@ -127,7 +107,10 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
};
|
};
|
||||||
const result = await updateTag({
|
const result = await updateTag({
|
||||||
variables: {
|
variables: {
|
||||||
input: getTagInput(input) as GQL.TagUpdateInput,
|
input: {
|
||||||
|
id: tag.id,
|
||||||
|
...input,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.tagUpdate) {
|
if (result.data?.tagUpdate) {
|
||||||
@@ -270,7 +253,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="tag-details col-md-4">
|
<div className="tag-details col-md-4">
|
||||||
<div className="text-center logo-container">
|
<div className="text-center logo-container">
|
||||||
{imageEncoding ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
@@ -303,6 +286,7 @@ const TagPage: React.FC<IProps> = ({ tag }) => {
|
|||||||
onCancel={onToggleEdit}
|
onCancel={onToggleEdit}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
setImage={setImage}
|
setImage={setImage}
|
||||||
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { useHistory, useLocation } from "react-router-dom";
|
|||||||
|
|
||||||
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 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";
|
import { useToast } from "src/hooks/Toast";
|
||||||
import { tagRelationHook } from "src/core/tags";
|
import { tagRelationHook } from "src/core/tags";
|
||||||
@@ -21,38 +20,18 @@ const TagCreate: React.FC = () => {
|
|||||||
|
|
||||||
// Editing tag state
|
// Editing tag state
|
||||||
const [image, setImage] = useState<string | null>();
|
const [image, setImage] = useState<string | null>();
|
||||||
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
|
|
||||||
const [createTag] = useTagCreate();
|
const [createTag] = useTagCreate();
|
||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
async function onSave(input: GQL.TagCreateInput) {
|
||||||
setImage(imageData);
|
|
||||||
}
|
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
|
||||||
|
|
||||||
function getTagInput(
|
|
||||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
|
||||||
) {
|
|
||||||
const ret: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
|
||||||
...input,
|
|
||||||
image,
|
|
||||||
};
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
|
||||||
|
|
||||||
async function onSave(
|
|
||||||
input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>
|
|
||||||
) {
|
|
||||||
try {
|
try {
|
||||||
const oldRelations = {
|
const oldRelations = {
|
||||||
parents: [],
|
parents: [],
|
||||||
children: [],
|
children: [],
|
||||||
};
|
};
|
||||||
const result = await createTag({
|
const result = await createTag({
|
||||||
variables: {
|
variables: { input },
|
||||||
input: getTagInput(input) as GQL.TagCreateInput,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
if (result.data?.tagCreate?.id) {
|
if (result.data?.tagCreate?.id) {
|
||||||
const created = result.data.tagCreate;
|
const created = result.data.tagCreate;
|
||||||
@@ -77,7 +56,7 @@ const TagCreate: React.FC = () => {
|
|||||||
<div className="row">
|
<div className="row">
|
||||||
<div className="tag-details col-md-8">
|
<div className="tag-details col-md-8">
|
||||||
<div className="text-center logo-container">
|
<div className="text-center logo-container">
|
||||||
{imageEncoding ? (
|
{encodingImage ? (
|
||||||
<LoadingIndicator message="Encoding image..." />
|
<LoadingIndicator message="Encoding image..." />
|
||||||
) : (
|
) : (
|
||||||
renderImage()
|
renderImage()
|
||||||
@@ -89,6 +68,7 @@ const TagCreate: React.FC = () => {
|
|||||||
onCancel={() => history.push("/tags")}
|
onCancel={() => history.push("/tags")}
|
||||||
onDelete={() => {}}
|
onDelete={() => {}}
|
||||||
setImage={setImage}
|
setImage={setImage}
|
||||||
|
setEncodingImage={setEncodingImage}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -11,14 +11,16 @@ 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 { StringListInput } from "src/components/Shared/StringListInput";
|
import { StringListInput } from "src/components/Shared/StringListInput";
|
||||||
|
import isEqual from "lodash-es/isEqual";
|
||||||
|
|
||||||
interface ITagEditPanel {
|
interface ITagEditPanel {
|
||||||
tag: Partial<GQL.TagDataFragment>;
|
tag: Partial<GQL.TagDataFragment>;
|
||||||
// returns id
|
// returns id
|
||||||
onSubmit: (tag: Partial<GQL.TagCreateInput | GQL.TagUpdateInput>) => void;
|
onSubmit: (tag: GQL.TagCreateInput) => void;
|
||||||
onCancel: () => void;
|
onCancel: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
setImage: (image?: string | null) => void;
|
setImage: (image?: string | null) => void;
|
||||||
|
setEncodingImage: (loading: boolean) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
||||||
@@ -27,6 +29,7 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
onCancel,
|
onCancel,
|
||||||
onDelete,
|
onDelete,
|
||||||
setImage,
|
setImage,
|
||||||
|
setEncodingImage,
|
||||||
}) => {
|
}) => {
|
||||||
const intl = useIntl();
|
const intl = useIntl();
|
||||||
|
|
||||||
@@ -39,46 +42,46 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
|
|
||||||
const schema = yup.object({
|
const schema = yup.object({
|
||||||
name: yup.string().required(),
|
name: yup.string().required(),
|
||||||
description: yup.string().optional().nullable(),
|
|
||||||
aliases: yup
|
aliases: yup
|
||||||
.array(yup.string().required())
|
.array(yup.string().required())
|
||||||
.optional()
|
.defined()
|
||||||
.test({
|
.test({
|
||||||
name: "unique",
|
name: "unique",
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
test: (value, context) => {
|
||||||
test: (value: any) => {
|
if (!value) return true;
|
||||||
return (value ?? []).length === new Set(value).size;
|
const aliases = new Set(value);
|
||||||
|
aliases.add(context.parent.name);
|
||||||
|
return value.length + 1 === aliases.size;
|
||||||
},
|
},
|
||||||
message: intl.formatMessage({ id: "dialogs.aliases_must_be_unique" }),
|
message: intl.formatMessage({
|
||||||
|
id: "validation.aliases_must_be_unique",
|
||||||
}),
|
}),
|
||||||
parent_ids: yup.array(yup.string().required()).optional().nullable(),
|
}),
|
||||||
child_ids: yup.array(yup.string().required()).optional().nullable(),
|
description: yup.string().ensure(),
|
||||||
ignore_auto_tag: yup.boolean().optional(),
|
parent_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
child_ids: yup.array(yup.string().required()).defined(),
|
||||||
|
ignore_auto_tag: yup.boolean().defined(),
|
||||||
|
image: yup.string().nullable().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
name: tag?.name,
|
name: tag?.name ?? "",
|
||||||
description: tag?.description,
|
aliases: tag?.aliases ?? [],
|
||||||
aliases: tag?.aliases,
|
description: tag?.description ?? "",
|
||||||
parent_ids: (tag?.parents ?? []).map((t) => t.id),
|
parent_ids: (tag?.parents ?? []).map((t) => t.id),
|
||||||
child_ids: (tag?.children ?? []).map((t) => t.id),
|
child_ids: (tag?.children ?? []).map((t) => t.id),
|
||||||
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
|
ignore_auto_tag: tag?.ignore_auto_tag ?? false,
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = yup.InferType<typeof schema>;
|
||||||
|
|
||||||
const formik = useFormik({
|
const formik = useFormik<InputValues>({
|
||||||
initialValues,
|
initialValues,
|
||||||
validationSchema: schema,
|
validationSchema: schema,
|
||||||
enableReinitialize: true,
|
enableReinitialize: true,
|
||||||
onSubmit: (values) => onSubmit(getTagInput(values)),
|
onSubmit: (values) => onSubmit(values),
|
||||||
});
|
});
|
||||||
|
|
||||||
// always dirty if creating a new tag with a name
|
|
||||||
if (isNew && tag?.name) {
|
|
||||||
formik.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
// set up hotkeys
|
// set up hotkeys
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Mousetrap.bind("s s", () => formik.handleSubmit());
|
Mousetrap.bind("s s", () => formik.handleSubmit());
|
||||||
@@ -88,19 +91,22 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function getTagInput(values: InputValues) {
|
const encodingImage = ImageUtils.usePasteImage(onImageLoad);
|
||||||
const input: Partial<GQL.TagCreateInput | GQL.TagUpdateInput> = {
|
|
||||||
...values,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (tag && tag.id) {
|
useEffect(() => {
|
||||||
(input as GQL.TagUpdateInput).id = tag.id;
|
setImage(formik.values.image);
|
||||||
}
|
}, [formik.values.image, setImage]);
|
||||||
return input;
|
|
||||||
|
useEffect(() => {
|
||||||
|
setEncodingImage(encodingImage);
|
||||||
|
}, [setEncodingImage, encodingImage]);
|
||||||
|
|
||||||
|
function onImageLoad(imageData: string | null) {
|
||||||
|
formik.setFieldValue("image", imageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, setImage);
|
ImageUtils.onImageChange(event, onImageLoad);
|
||||||
}
|
}
|
||||||
|
|
||||||
const isEditing = true;
|
const isEditing = true;
|
||||||
@@ -120,7 +126,8 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
<Prompt
|
<Prompt
|
||||||
when={formik.dirty}
|
when={formik.dirty}
|
||||||
message={(location, action) => {
|
message={(location, action) => {
|
||||||
if (action === "PUSH" && location.pathname.startsWith(`/tags/`)) {
|
// Check if it's a redirect after movie creation
|
||||||
|
if (action === "PUSH" && location.pathname.startsWith("/tags/")) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return intl.formatMessage({ id: "dialogs.unsaved_changes" });
|
return intl.formatMessage({ id: "dialogs.unsaved_changes" });
|
||||||
@@ -151,9 +158,13 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<StringListInput
|
<StringListInput
|
||||||
value={formik.values.aliases ?? []}
|
value={formik.values.aliases}
|
||||||
setValue={(value) => formik.setFieldValue("aliases", value)}
|
setValue={(value) => formik.setFieldValue("aliases", value)}
|
||||||
errors={formik.errors.aliases}
|
errors={
|
||||||
|
Array.isArray(formik.errors.aliases)
|
||||||
|
? formik.errors.aliases[0]
|
||||||
|
: formik.errors.aliases
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -191,9 +202,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
ids={formik.values.parent_ids}
|
ids={formik.values.parent_ids}
|
||||||
excludeIds={(tag?.id ? [tag.id] : []).concat(
|
excludeIds={[
|
||||||
...formik.values.child_ids
|
...(tag?.id ? [tag.id] : []),
|
||||||
)}
|
...formik.values.child_ids,
|
||||||
|
]}
|
||||||
creatable={false}
|
creatable={false}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -218,9 +230,10 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
ids={formik.values.child_ids}
|
ids={formik.values.child_ids}
|
||||||
excludeIds={(tag?.id ? [tag.id] : []).concat(
|
excludeIds={[
|
||||||
...formik.values.parent_ids
|
...(tag?.id ? [tag.id] : []),
|
||||||
)}
|
...formik.values.parent_ids,
|
||||||
|
]}
|
||||||
creatable={false}
|
creatable={false}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
@@ -248,13 +261,11 @@ export const TagEditPanel: React.FC<ITagEditPanel> = ({
|
|||||||
isNew={isNew}
|
isNew={isNew}
|
||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={() => formik.handleSubmit()}
|
onSave={formik.handleSubmit}
|
||||||
saveDisabled={!formik.dirty}
|
saveDisabled={(!isNew && !formik.dirty) || !isEqual(formik.errors, {})}
|
||||||
onImageChange={onImageChange}
|
onImageChange={onImageChange}
|
||||||
onImageChangeURL={setImage}
|
onImageChangeURL={onImageLoad}
|
||||||
onClearImage={() => {
|
onClearImage={() => onImageLoad(null)}
|
||||||
setImage(null);
|
|
||||||
}}
|
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
acceptSVG
|
acceptSVG
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
* Overhauled and improved HLS streaming. ([#3274](https://github.com/stashapp/stash/pull/3274))
|
* Overhauled and improved HLS streaming. ([#3274](https://github.com/stashapp/stash/pull/3274))
|
||||||
|
|
||||||
### 🐛 Bug fixes
|
### 🐛 Bug fixes
|
||||||
|
* Fixed Save button being disabled when stting Tag image. ([#3509](https://github.com/stashapp/stash/pull/3509))
|
||||||
* Fixed incorrect performer with identical name being matched when scraping from stash-box. ([#3488](https://github.com/stashapp/stash/pull/3488))
|
* Fixed incorrect performer with identical name being matched when scraping from stash-box. ([#3488](https://github.com/stashapp/stash/pull/3488))
|
||||||
* Fixed scene cover not being included when submitting file-less scenes to stash-box. ([#3465](https://github.com/stashapp/stash/pull/3465))
|
* Fixed scene cover not being included when submitting file-less scenes to stash-box. ([#3465](https://github.com/stashapp/stash/pull/3465))
|
||||||
* Fixed URL not being during stash-box scrape if the Studio URL is not set. ([#3439](https://github.com/stashapp/stash/pull/3439))
|
* Fixed URL not being during stash-box scrape if the Studio URL is not set. ([#3439](https://github.com/stashapp/stash/pull/3439))
|
||||||
|
|||||||
@@ -646,7 +646,6 @@
|
|||||||
"details": "Detaily",
|
"details": "Detaily",
|
||||||
"developmentVersion": "Vývojářská verze",
|
"developmentVersion": "Vývojářská verze",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "aliasy musí být unikátní",
|
|
||||||
"delete_alert": "Následující {count, plural, one {{singularEntity}} other {{pluralEntity}}} budou permanentně smazány:",
|
"delete_alert": "Následující {count, plural, one {{singularEntity}} other {{pluralEntity}}} budou permanentně smazány:",
|
||||||
"delete_confirm": "Jste si jisti, že chcete smazat {entityName}?",
|
"delete_confirm": "Jste si jisti, že chcete smazat {entityName}?",
|
||||||
"delete_entity_desc": "{count, plural, one {Jste si jisti, že checete smazat toto {singularEntity}? Pokud není soubor rovněž smazán, tato {singularEntity} bude znovu přidána při příštím skenování.} other {Jste si jisti, že chcete smazat tyto {pluralEntity}? Pokud nejsou soubory rovněž smazány, tyto {pluralEntity} budou znovu přidány při příštím skenování.}}",
|
"delete_entity_desc": "{count, plural, one {Jste si jisti, že checete smazat toto {singularEntity}? Pokud není soubor rovněž smazán, tato {singularEntity} bude znovu přidána při příštím skenování.} other {Jste si jisti, že chcete smazat tyto {pluralEntity}? Pokud nejsou soubory rovněž smazány, tyto {pluralEntity} budou znovu přidány při příštím skenování.}}",
|
||||||
@@ -696,5 +695,8 @@
|
|||||||
"marker_image_previews_tooltip": "Animované WebP náhledy markerů, nezbytné pouze tehdy, pokud je Typ náhledu nastaven na \"Animovaný obrázek\".",
|
"marker_image_previews_tooltip": "Animované WebP náhledy markerů, nezbytné pouze tehdy, pokud je Typ náhledu nastaven na \"Animovaný obrázek\".",
|
||||||
"marker_screenshots": "Screenshoty markerů"
|
"marker_screenshots": "Screenshoty markerů"
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "aliasy musí být unikátní"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -614,7 +614,6 @@
|
|||||||
"details": "Detaljer",
|
"details": "Detaljer",
|
||||||
"developmentVersion": "Udviklingsversion",
|
"developmentVersion": "Udviklingsversion",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "aliaser skal være unikke",
|
|
||||||
"delete_alert": "Følgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} vil blive slettet permanent:",
|
"delete_alert": "Følgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} vil blive slettet permanent:",
|
||||||
"delete_confirm": "Er du sikker på, at du vil slette {entityName}?",
|
"delete_confirm": "Er du sikker på, at du vil slette {entityName}?",
|
||||||
"delete_entity_desc": "{count, plural, one {Er du sikker på, at du vil slette denne {singularEntity}? Medmindre filen også slettes, vil denne {singularEntity} blive tilføjet igen, når scanningen udføres.} andet {Er du sikker på, at du vil slette disse {pluralEntity}? Medmindre filerne også slettes, vil disse {pluralEntity} blive tilføjet igen, når scanningen udføres.}}",
|
"delete_entity_desc": "{count, plural, one {Er du sikker på, at du vil slette denne {singularEntity}? Medmindre filen også slettes, vil denne {singularEntity} blive tilføjet igen, når scanningen udføres.} andet {Er du sikker på, at du vil slette disse {pluralEntity}? Medmindre filerne også slettes, vil disse {pluralEntity} blive tilføjet igen, når scanningen udføres.}}",
|
||||||
@@ -1039,6 +1038,9 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"updated_at": "Opdateret Den",
|
"updated_at": "Opdateret Den",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "aliaser skal være unikke"
|
||||||
|
},
|
||||||
"videos": "Videoer",
|
"videos": "Videoer",
|
||||||
"view_all": "Se alle",
|
"view_all": "Se alle",
|
||||||
"weight": "Vægt",
|
"weight": "Vægt",
|
||||||
|
|||||||
@@ -662,7 +662,6 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"developmentVersion": "Entwicklungsversion",
|
"developmentVersion": "Entwicklungsversion",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "Aliase müssen einzigartig sein",
|
|
||||||
"create_new_entity": "Neues {entity} erstellen",
|
"create_new_entity": "Neues {entity} erstellen",
|
||||||
"delete_alert": "Folgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} werden dauerhaft gelöscht:",
|
"delete_alert": "Folgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} werden dauerhaft gelöscht:",
|
||||||
"delete_confirm": "Möchten Sie {entityName} wirklich löschen?",
|
"delete_confirm": "Möchten Sie {entityName} wirklich löschen?",
|
||||||
@@ -1118,6 +1117,9 @@
|
|||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"updated_at": "Aktualisiert am",
|
"updated_at": "Aktualisiert am",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "Aliase müssen einzigartig sein"
|
||||||
|
},
|
||||||
"videos": "Videos",
|
"videos": "Videos",
|
||||||
"view_all": "Alle ansehen",
|
"view_all": "Alle ansehen",
|
||||||
"weight": "Gewicht",
|
"weight": "Gewicht",
|
||||||
|
|||||||
@@ -699,7 +699,6 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"developmentVersion": "Development Version",
|
"developmentVersion": "Development Version",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "aliases must be unique",
|
|
||||||
"create_new_entity": "Create new {entity}",
|
"create_new_entity": "Create new {entity}",
|
||||||
"delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:",
|
"delete_alert": "The following {count, plural, one {{singularEntity}} other {{pluralEntity}}} will be deleted permanently:",
|
||||||
"delete_confirm": "Are you sure you want to delete {entityName}?",
|
"delete_confirm": "Are you sure you want to delete {entityName}?",
|
||||||
@@ -1164,6 +1163,11 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"updated_at": "Updated At",
|
"updated_at": "Updated At",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "aliases must be unique",
|
||||||
|
"date_invalid_form": "${path} must be in YYYY-MM-DD form",
|
||||||
|
"required": "${path} is a required field"
|
||||||
|
},
|
||||||
"videos": "Videos",
|
"videos": "Videos",
|
||||||
"view_all": "View All",
|
"view_all": "View All",
|
||||||
"play_count": "Play Count",
|
"play_count": "Play Count",
|
||||||
|
|||||||
@@ -607,7 +607,6 @@
|
|||||||
"details": "Detalles",
|
"details": "Detalles",
|
||||||
"developmentVersion": "Versión de desarrollo",
|
"developmentVersion": "Versión de desarrollo",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "Los alias deben ser únicos",
|
|
||||||
"delete_alert": "El/los siguiente/s {count, plural, one {{singularEntity}} other {{pluralEntity}}} se eliminará/n de forma permanente:",
|
"delete_alert": "El/los siguiente/s {count, plural, one {{singularEntity}} other {{pluralEntity}}} se eliminará/n de forma permanente:",
|
||||||
"delete_confirm": "¿Estás seguro que deseas eliminar {entityName}?",
|
"delete_confirm": "¿Estás seguro que deseas eliminar {entityName}?",
|
||||||
"delete_entity_desc": "{count, plural, one {¿Estás seguro que deseas eliminar esta {singularEntity}? Hasta que el archivo sea eliminado también, esta {singularEntity} se volverá a añadir cuando se lleve a cabo un escaneo.} other {¿Estás seguro que deseas eliminar {pluralEntity}? Hasta que los archivos sean eliminados del sistema de ficheros también, estas {pluralEntity} se volverán a añadir cuando se lleve a cabo un escaneo.}}",
|
"delete_entity_desc": "{count, plural, one {¿Estás seguro que deseas eliminar esta {singularEntity}? Hasta que el archivo sea eliminado también, esta {singularEntity} se volverá a añadir cuando se lleve a cabo un escaneo.} other {¿Estás seguro que deseas eliminar {pluralEntity}? Hasta que los archivos sean eliminados del sistema de ficheros también, estas {pluralEntity} se volverán a añadir cuando se lleve a cabo un escaneo.}}",
|
||||||
@@ -1017,6 +1016,9 @@
|
|||||||
"twitter": "Twitter",
|
"twitter": "Twitter",
|
||||||
"updated_at": "Fecha de modificación",
|
"updated_at": "Fecha de modificación",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "Los alias deben ser únicos"
|
||||||
|
},
|
||||||
"videos": "Vídeos",
|
"videos": "Vídeos",
|
||||||
"weight": "Peso",
|
"weight": "Peso",
|
||||||
"years_old": "años"
|
"years_old": "años"
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"details": "Detailid",
|
"details": "Detailid",
|
||||||
"developmentVersion": "Arendusversioon",
|
"developmentVersion": "Arendusversioon",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "aliased peavad olema erilised",
|
|
||||||
"create_new_entity": "Loo uus {entity}",
|
"create_new_entity": "Loo uus {entity}",
|
||||||
"delete_alert": "Järgnev {count, plural, one {{singularEntity}} other {{pluralEntity}}} kustutatakse lõplikult:",
|
"delete_alert": "Järgnev {count, plural, one {{singularEntity}} other {{pluralEntity}}} kustutatakse lõplikult:",
|
||||||
"delete_confirm": "Kas oled kindel, et soovid kustutada {entityName}?",
|
"delete_confirm": "Kas oled kindel, et soovid kustutada {entityName}?",
|
||||||
@@ -1124,6 +1123,9 @@
|
|||||||
"type": "Tüüp",
|
"type": "Tüüp",
|
||||||
"updated_at": "Viimati Uuendatud",
|
"updated_at": "Viimati Uuendatud",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "aliased peavad olema erilised"
|
||||||
|
},
|
||||||
"videos": "Videod",
|
"videos": "Videod",
|
||||||
"view_all": "Vaata Kõiki",
|
"view_all": "Vaata Kõiki",
|
||||||
"weight": "Kaal",
|
"weight": "Kaal",
|
||||||
|
|||||||
@@ -614,7 +614,6 @@
|
|||||||
"details": "Lisätiedot",
|
"details": "Lisätiedot",
|
||||||
"developmentVersion": "Kehitysversio",
|
"developmentVersion": "Kehitysversio",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "Aliaksien pitää olla uniikkeja",
|
|
||||||
"create_new_entity": "Luo uus {entity}",
|
"create_new_entity": "Luo uus {entity}",
|
||||||
"delete_alert": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} poistetaan pysyvästi:",
|
"delete_alert": "{count, plural, one {{singularEntity}} other {{pluralEntity}}} poistetaan pysyvästi:",
|
||||||
"delete_confirm": "Haluatko varmasti poistaa {entityName}?",
|
"delete_confirm": "Haluatko varmasti poistaa {entityName}?",
|
||||||
@@ -1028,6 +1027,9 @@
|
|||||||
"twitter": "Twitter",
|
"twitter": "Twitter",
|
||||||
"updated_at": "Päivitetty",
|
"updated_at": "Päivitetty",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "Aliaksien pitää olla uniikkeja"
|
||||||
|
},
|
||||||
"videos": "Videot",
|
"videos": "Videot",
|
||||||
"view_all": "Näytä kaikki",
|
"view_all": "Näytä kaikki",
|
||||||
"weight": "Paino",
|
"weight": "Paino",
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"details": "Détails",
|
"details": "Détails",
|
||||||
"developmentVersion": "Version de développement",
|
"developmentVersion": "Version de développement",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "Les alias doivent être uniques",
|
|
||||||
"create_new_entity": "Créer un nouveau {entity}",
|
"create_new_entity": "Créer un nouveau {entity}",
|
||||||
"delete_alert": "{count, plural, one {Le {singularEntity} suivant sera supprimé} other {Les {pluralEntity} suivants seront supprimés}} définitivement :",
|
"delete_alert": "{count, plural, one {Le {singularEntity} suivant sera supprimé} other {Les {pluralEntity} suivants seront supprimés}} définitivement :",
|
||||||
"delete_confirm": "Êtes-vous sûr de vouloir supprimer {entityName} ?",
|
"delete_confirm": "Êtes-vous sûr de vouloir supprimer {entityName} ?",
|
||||||
@@ -1124,6 +1123,9 @@
|
|||||||
"type": "Type",
|
"type": "Type",
|
||||||
"updated_at": "Actualisé le",
|
"updated_at": "Actualisé le",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "Les alias doivent être uniques"
|
||||||
|
},
|
||||||
"videos": "Vidéos",
|
"videos": "Vidéos",
|
||||||
"view_all": "Tout voir",
|
"view_all": "Tout voir",
|
||||||
"weight": "Poids",
|
"weight": "Poids",
|
||||||
|
|||||||
@@ -668,7 +668,6 @@
|
|||||||
"details": "Dettagli",
|
"details": "Dettagli",
|
||||||
"developmentVersion": "Versione Sviluppo",
|
"developmentVersion": "Versione Sviluppo",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "gli alias devono essere univoci",
|
|
||||||
"create_new_entity": "Crea nuovo/a {entity}",
|
"create_new_entity": "Crea nuovo/a {entity}",
|
||||||
"delete_alert": "Il seguente/I seguenti {count, plural, one {{singularEntity}} other {{pluralEntity}}} sarà/saranno cancellati permanentemente:",
|
"delete_alert": "Il seguente/I seguenti {count, plural, one {{singularEntity}} other {{pluralEntity}}} sarà/saranno cancellati permanentemente:",
|
||||||
"delete_confirm": "Sei sicuro di voler cancellare {entityName}?",
|
"delete_confirm": "Sei sicuro di voler cancellare {entityName}?",
|
||||||
@@ -1126,6 +1125,9 @@
|
|||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"updated_at": "Aggiornato Al",
|
"updated_at": "Aggiornato Al",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "gli alias devono essere univoci"
|
||||||
|
},
|
||||||
"videos": "Video",
|
"videos": "Video",
|
||||||
"view_all": "Vedi Tutto",
|
"view_all": "Vedi Tutto",
|
||||||
"weight": "Peso",
|
"weight": "Peso",
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"details": "詳細",
|
"details": "詳細",
|
||||||
"developmentVersion": "開発者バージョン",
|
"developmentVersion": "開発者バージョン",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "別名は一意でなければいけません",
|
|
||||||
"create_new_entity": "{entity}を新規作成する",
|
"create_new_entity": "{entity}を新規作成する",
|
||||||
"delete_alert": "次の{count, plural, one {{singularEntity}} other {{pluralEntity}}}は完全に削除されます:",
|
"delete_alert": "次の{count, plural, one {{singularEntity}} other {{pluralEntity}}}は完全に削除されます:",
|
||||||
"delete_confirm": "本当に{entityName}を削除してよろしいですか?",
|
"delete_confirm": "本当に{entityName}を削除してよろしいですか?",
|
||||||
@@ -1124,6 +1123,9 @@
|
|||||||
"type": "タイプ",
|
"type": "タイプ",
|
||||||
"updated_at": "更新日:",
|
"updated_at": "更新日:",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "別名は一意でなければいけません"
|
||||||
|
},
|
||||||
"videos": "動画",
|
"videos": "動画",
|
||||||
"view_all": "全て表示",
|
"view_all": "全て表示",
|
||||||
"weight": "幅",
|
"weight": "幅",
|
||||||
|
|||||||
@@ -661,7 +661,6 @@
|
|||||||
"details": "세부사항",
|
"details": "세부사항",
|
||||||
"developmentVersion": "개발 버전",
|
"developmentVersion": "개발 버전",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "별칭은 유일해야 합니다",
|
|
||||||
"create_new_entity": "새로운 {entity} 생성",
|
"create_new_entity": "새로운 {entity} 생성",
|
||||||
"delete_alert": "다음 {count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 영구 삭제될 것입니다:",
|
"delete_alert": "다음 {count, plural, one {{singularEntity}} other {{pluralEntity}}}이(가) 영구 삭제될 것입니다:",
|
||||||
"delete_confirm": "정말 {entityName}을 삭제하시겠습니까?",
|
"delete_confirm": "정말 {entityName}을 삭제하시겠습니까?",
|
||||||
@@ -1107,6 +1106,9 @@
|
|||||||
"type": "유형",
|
"type": "유형",
|
||||||
"updated_at": "수정 날짜",
|
"updated_at": "수정 날짜",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "별칭은 유일해야 합니다"
|
||||||
|
},
|
||||||
"videos": "비디오",
|
"videos": "비디오",
|
||||||
"view_all": "모두 보기",
|
"view_all": "모두 보기",
|
||||||
"weight": "몸무게",
|
"weight": "몸무게",
|
||||||
|
|||||||
@@ -590,7 +590,6 @@
|
|||||||
"details": "Details",
|
"details": "Details",
|
||||||
"developmentVersion": "Ontwikkelingsversie",
|
"developmentVersion": "Ontwikkelingsversie",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "aliases moeten uniek zijn",
|
|
||||||
"delete_alert": "De volgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} zal permanent verwijderd worden:",
|
"delete_alert": "De volgende {count, plural, one {{singularEntity}} other {{pluralEntity}}} zal permanent verwijderd worden:",
|
||||||
"delete_confirm": "Weet je zeker dat je {entityName} wilt verwijderen?",
|
"delete_confirm": "Weet je zeker dat je {entityName} wilt verwijderen?",
|
||||||
"delete_entity_desc": "{count, plural, one {Weet u zeker dat u deze {singularEntity} wilt verwijderen? Tenzij het bestand ook wordt verwijderd, wordt deze {singularEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.} other {Weet u zeker dat u deze {pluralEntity} wilt verwijderen? Tenzij de bestanden ook worden verwijderd, worden deze {pluralEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.}}",
|
"delete_entity_desc": "{count, plural, one {Weet u zeker dat u deze {singularEntity} wilt verwijderen? Tenzij het bestand ook wordt verwijderd, wordt deze {singularEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.} other {Weet u zeker dat u deze {pluralEntity} wilt verwijderen? Tenzij de bestanden ook worden verwijderd, worden deze {pluralEntity} opnieuw toegevoegd wanneer de scan wordt uitgevoerd.}}",
|
||||||
@@ -988,6 +987,9 @@
|
|||||||
"twitter": "Twitter",
|
"twitter": "Twitter",
|
||||||
"updated_at": "Bijgewerkt op",
|
"updated_at": "Bijgewerkt op",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "aliases moeten uniek zijn"
|
||||||
|
},
|
||||||
"videos": "Video's",
|
"videos": "Video's",
|
||||||
"view_all": "Alles weergeven",
|
"view_all": "Alles weergeven",
|
||||||
"weight": "Gewicht",
|
"weight": "Gewicht",
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"details": "Szczegóły",
|
"details": "Szczegóły",
|
||||||
"developmentVersion": "Wersja deweloperska",
|
"developmentVersion": "Wersja deweloperska",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "aliasy muszą być unikatowe",
|
|
||||||
"create_new_entity": "Dodaj {entity}",
|
"create_new_entity": "Dodaj {entity}",
|
||||||
"delete_alert": "Następujące elementy {count, plural, one {{singularEntity}} other {{pluralEntity}}} zostaną trwale usunięte:",
|
"delete_alert": "Następujące elementy {count, plural, one {{singularEntity}} other {{pluralEntity}}} zostaną trwale usunięte:",
|
||||||
"delete_confirm": "Czy na pewno chcesz usunąć {entityName}?",
|
"delete_confirm": "Czy na pewno chcesz usunąć {entityName}?",
|
||||||
@@ -1124,6 +1123,9 @@
|
|||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"updated_at": "Zaktualizowano",
|
"updated_at": "Zaktualizowano",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "aliasy muszą być unikatowe"
|
||||||
|
},
|
||||||
"videos": "Filmy wideo",
|
"videos": "Filmy wideo",
|
||||||
"view_all": "Pokaż wszystko",
|
"view_all": "Pokaż wszystko",
|
||||||
"weight": "Waga",
|
"weight": "Waga",
|
||||||
|
|||||||
@@ -592,7 +592,6 @@
|
|||||||
"details": "Detalhes",
|
"details": "Detalhes",
|
||||||
"developmentVersion": "Versão de desenvolvimento",
|
"developmentVersion": "Versão de desenvolvimento",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "apelidos devem ser únicos",
|
|
||||||
"delete_alert": "Os seguintes {count, plural, one {{singularEntity}} other {{pluralEntity}}} serão deletados permanentemente:",
|
"delete_alert": "Os seguintes {count, plural, one {{singularEntity}} other {{pluralEntity}}} serão deletados permanentemente:",
|
||||||
"delete_confirm": "Tem certeza de que deseja excluir {entityName}?",
|
"delete_confirm": "Tem certeza de que deseja excluir {entityName}?",
|
||||||
"delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também esteja excluído, este(a) {singularEntity} será re-adicionado quando a escaneamento for executado.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também estejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando o escaneamento for executado.}}",
|
"delete_entity_desc": "{count, plural, one {Tem certeza de que deseja excluir este(a) {singularEntity}? A menos que o arquivo também esteja excluído, este(a) {singularEntity} será re-adicionado quando a escaneamento for executado.} other {Tem certeza de que deseja excluir estes(as) {pluralEntity}? A menos que os arquivos também estejam excluídos, estes(as) {pluralEntity} serão re-adicionados quando o escaneamento for executado.}}",
|
||||||
@@ -1021,6 +1020,9 @@
|
|||||||
"type": "Tipo",
|
"type": "Tipo",
|
||||||
"updated_at": "Atualizado em",
|
"updated_at": "Atualizado em",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "apelidos devem ser únicos"
|
||||||
|
},
|
||||||
"videos": "Vídeos",
|
"videos": "Vídeos",
|
||||||
"view_all": "Ver todos",
|
"view_all": "Ver todos",
|
||||||
"weight": "Peso",
|
"weight": "Peso",
|
||||||
|
|||||||
@@ -661,7 +661,6 @@
|
|||||||
"details": "Подробности",
|
"details": "Подробности",
|
||||||
"developmentVersion": "Версия разработки",
|
"developmentVersion": "Версия разработки",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "псевдонимы должны быть уникальными",
|
|
||||||
"create_new_entity": "Создать новую запись в {entity}",
|
"create_new_entity": "Создать новую запись в {entity}",
|
||||||
"delete_alert": "Следующие {count, plural, one {{singularEntity}} other {{pluralEntity}}} будут удалены безвозвратно:",
|
"delete_alert": "Следующие {count, plural, one {{singularEntity}} other {{pluralEntity}}} будут удалены безвозвратно:",
|
||||||
"delete_confirm": "Вы уверены что хотите удалить {entityName}?",
|
"delete_confirm": "Вы уверены что хотите удалить {entityName}?",
|
||||||
@@ -1119,6 +1118,9 @@
|
|||||||
"type": "Тип",
|
"type": "Тип",
|
||||||
"updated_at": "Обновлено",
|
"updated_at": "Обновлено",
|
||||||
"url": "Ссылка",
|
"url": "Ссылка",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "псевдонимы должны быть уникальными"
|
||||||
|
},
|
||||||
"videos": "Видео",
|
"videos": "Видео",
|
||||||
"view_all": "Показать все",
|
"view_all": "Показать все",
|
||||||
"weight": "Вес",
|
"weight": "Вес",
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"details": "Beskrivningar",
|
"details": "Beskrivningar",
|
||||||
"developmentVersion": "Utvecklingsversion",
|
"developmentVersion": "Utvecklingsversion",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "alias måste vara unik",
|
|
||||||
"create_new_entity": "Skapa ny {entity}",
|
"create_new_entity": "Skapa ny {entity}",
|
||||||
"delete_alert": "De följande {count, plural, one {{singularEntity}} andra {{pluralEntity}}} kommer raderas permanent:",
|
"delete_alert": "De följande {count, plural, one {{singularEntity}} andra {{pluralEntity}}} kommer raderas permanent:",
|
||||||
"delete_confirm": "Är du säker på att du vill radera {entityName}?",
|
"delete_confirm": "Är du säker på att du vill radera {entityName}?",
|
||||||
@@ -1124,6 +1123,9 @@
|
|||||||
"type": "Typ",
|
"type": "Typ",
|
||||||
"updated_at": "Uppdaterad vid",
|
"updated_at": "Uppdaterad vid",
|
||||||
"url": "URL",
|
"url": "URL",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "alias måste vara unik"
|
||||||
|
},
|
||||||
"videos": "Videor",
|
"videos": "Videor",
|
||||||
"view_all": "Visa Allt",
|
"view_all": "Visa Allt",
|
||||||
"weight": "Vikt",
|
"weight": "Vikt",
|
||||||
|
|||||||
@@ -547,7 +547,6 @@
|
|||||||
"details": "Ayrıntılar",
|
"details": "Ayrıntılar",
|
||||||
"developmentVersion": "Geliştirme Sürümü",
|
"developmentVersion": "Geliştirme Sürümü",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "takma adlar benzersiz olmalıdır",
|
|
||||||
"delete_alert": "Bu {count, plural, one {{singularEntity}} other {{pluralEntity}}} kalıcı olarak silinecektir:",
|
"delete_alert": "Bu {count, plural, one {{singularEntity}} other {{pluralEntity}}} kalıcı olarak silinecektir:",
|
||||||
"delete_confirm": "Bunu silmek istediğinizden emin misiniz: {entityName}?",
|
"delete_confirm": "Bunu silmek istediğinizden emin misiniz: {entityName}?",
|
||||||
"delete_entity_desc": "{count, plural, one {Bunu silmek istediğinizden emin misiniz: {entityName}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {singularEntity} tekrar veritabanına eklenecektir.} other {Bunları silmek istediğinizden emin misiniz: {pluralEntity}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {pluralEntity} tekrar veritabanına eklenecektir.}}",
|
"delete_entity_desc": "{count, plural, one {Bunu silmek istediğinizden emin misiniz: {entityName}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {singularEntity} tekrar veritabanına eklenecektir.} other {Bunları silmek istediğinizden emin misiniz: {pluralEntity}? Dosyayı bilgisayarınızdan silmediğiniz sürece, yeniden tarama işleminde bu {pluralEntity} tekrar veritabanına eklenecektir.}}",
|
||||||
@@ -891,6 +890,9 @@
|
|||||||
"twitter": "Twitter",
|
"twitter": "Twitter",
|
||||||
"updated_at": "Güncellenme Zamanı",
|
"updated_at": "Güncellenme Zamanı",
|
||||||
"url": "Internet Adresi (URL)",
|
"url": "Internet Adresi (URL)",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "takma adlar benzersiz olmalıdır"
|
||||||
|
},
|
||||||
"videos": "Videolar",
|
"videos": "Videolar",
|
||||||
"weight": "Kilo",
|
"weight": "Kilo",
|
||||||
"years_old": "yaşında"
|
"years_old": "yaşında"
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"details": "简介",
|
"details": "简介",
|
||||||
"developmentVersion": "开发版本",
|
"developmentVersion": "开发版本",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "别名必须是唯一的",
|
|
||||||
"create_new_entity": "创建新的 {entity}",
|
"create_new_entity": "创建新的 {entity}",
|
||||||
"delete_alert": "以下 {count, plural, one {{singularEntity}} other {{pluralEntity}}} 会被永久删除:",
|
"delete_alert": "以下 {count, plural, one {{singularEntity}} other {{pluralEntity}}} 会被永久删除:",
|
||||||
"delete_confirm": "确定要删除 {entityName} 吗?",
|
"delete_confirm": "确定要删除 {entityName} 吗?",
|
||||||
@@ -1124,6 +1123,9 @@
|
|||||||
"type": "类别",
|
"type": "类别",
|
||||||
"updated_at": "更新时间",
|
"updated_at": "更新时间",
|
||||||
"url": "链接",
|
"url": "链接",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "别名必须是唯一的"
|
||||||
|
},
|
||||||
"videos": "视频",
|
"videos": "视频",
|
||||||
"view_all": "查看全部",
|
"view_all": "查看全部",
|
||||||
"weight": "体重",
|
"weight": "体重",
|
||||||
|
|||||||
@@ -666,7 +666,6 @@
|
|||||||
"details": "細節",
|
"details": "細節",
|
||||||
"developmentVersion": "開發版本",
|
"developmentVersion": "開發版本",
|
||||||
"dialogs": {
|
"dialogs": {
|
||||||
"aliases_must_be_unique": "別名不可重複",
|
|
||||||
"create_new_entity": "建立新{entity}",
|
"create_new_entity": "建立新{entity}",
|
||||||
"delete_alert": "以下{count, plural, one {{singularEntity}} other {{pluralEntity}}}將被永久刪除:",
|
"delete_alert": "以下{count, plural, one {{singularEntity}} other {{pluralEntity}}}將被永久刪除:",
|
||||||
"delete_confirm": "你確定要刪除 {entityName} 嗎?",
|
"delete_confirm": "你確定要刪除 {entityName} 嗎?",
|
||||||
@@ -1124,6 +1123,9 @@
|
|||||||
"type": "種類",
|
"type": "種類",
|
||||||
"updated_at": "更新於",
|
"updated_at": "更新於",
|
||||||
"url": "連結",
|
"url": "連結",
|
||||||
|
"validation": {
|
||||||
|
"aliases_must_be_unique": "別名不可重複"
|
||||||
|
},
|
||||||
"videos": "影片",
|
"videos": "影片",
|
||||||
"view_all": "顯示全部",
|
"view_all": "顯示全部",
|
||||||
"weight": "體重",
|
"weight": "體重",
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export const stringGenderMap = new Map<string, GQL.GenderEnum>([
|
|||||||
["Non-Binary", GQL.GenderEnum.NonBinary],
|
["Non-Binary", GQL.GenderEnum.NonBinary],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const genderToString = (value?: GQL.GenderEnum | string) => {
|
export const genderToString = (value?: GQL.GenderEnum | string | null) => {
|
||||||
if (!value) {
|
if (!value) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user