mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Use formik for scene edit (#1429)
* Use formik for scene edit panel * Fix unsetting rating * Disable save if not dirty * Movie image fixes
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
|
* Added [DLNA server](/settings?tab=dlna). ([#1364](https://github.com/stashapp/stash/pull/1364))
|
||||||
|
|
||||||
### 🎨 Improvements
|
### 🎨 Improvements
|
||||||
|
* Prompt when leaving scene edit page with unsaved changed. ([#1429](https://github.com/stashapp/stash/pull/1429))
|
||||||
* Make multi-set mode buttons more obvious in multi-edit dialog. ([#1435](https://github.com/stashapp/stash/pull/1435))
|
* Make multi-set mode buttons more obvious in multi-edit dialog. ([#1435](https://github.com/stashapp/stash/pull/1435))
|
||||||
* Filter modifiers and sort by options are now sorted alphabetically. ([#1406](https://github.com/stashapp/stash/pull/1406))
|
* Filter modifiers and sort by options are now sorted alphabetically. ([#1406](https://github.com/stashapp/stash/pull/1406))
|
||||||
* Add `CreatedAt` and `UpdatedAt` (and `FileModTime` where applicable) to API objects. ([#1421](https://github.com/stashapp/stash/pull/1421))
|
* Add `CreatedAt` and `UpdatedAt` (and `FileModTime` where applicable) to API objects. ([#1421](https://github.com/stashapp/stash/pull/1421))
|
||||||
@@ -16,6 +17,7 @@
|
|||||||
* Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378))
|
* Add button to remove studio stash ID. ([#1378](https://github.com/stashapp/stash/pull/1378))
|
||||||
|
|
||||||
### 🐛 Bug fixes
|
### 🐛 Bug fixes
|
||||||
|
* Fix clearing Performer and Movie ratings not working. ([#1429](https://github.com/stashapp/stash/pull/1429))
|
||||||
* Fix scraper date parser failing when parsing time. ([#1431](https://github.com/stashapp/stash/pull/1431))
|
* Fix scraper date parser failing when parsing time. ([#1431](https://github.com/stashapp/stash/pull/1431))
|
||||||
* Fix quotes in filter labels causing UI errors. ([#1425](https://github.com/stashapp/stash/pull/1425))
|
* Fix quotes in filter labels causing UI errors. ([#1425](https://github.com/stashapp/stash/pull/1425))
|
||||||
* Fix post-processing not running when scraping by performer fragment. ([#1387](https://github.com/stashapp/stash/pull/1387))
|
* Fix post-processing not running when scraping by performer fragment. ([#1387](https://github.com/stashapp/stash/pull/1387))
|
||||||
|
|||||||
@@ -15,8 +15,6 @@ import {
|
|||||||
Modal,
|
Modal,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Modal as BSModal, Button } from "react-bootstrap";
|
|
||||||
import { ImageUtils } from "src/utils";
|
|
||||||
import { MovieScenesPanel } from "./MovieScenesPanel";
|
import { MovieScenesPanel } from "./MovieScenesPanel";
|
||||||
import { MovieDetailsPanel } from "./MovieDetailsPanel";
|
import { MovieDetailsPanel } from "./MovieDetailsPanel";
|
||||||
import { MovieEditPanel } from "./MovieEditPanel";
|
import { MovieEditPanel } from "./MovieEditPanel";
|
||||||
@@ -34,7 +32,6 @@ export const Movie: React.FC = () => {
|
|||||||
// Editing state
|
// Editing state
|
||||||
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
const [isEditing, setIsEditing] = useState<boolean>(isNew);
|
||||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||||
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
|
||||||
|
|
||||||
// Editing movie state
|
// Editing movie state
|
||||||
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
const [frontImage, setFrontImage] = useState<string | undefined | null>(
|
||||||
@@ -43,11 +40,7 @@ export const Movie: React.FC = () => {
|
|||||||
const [backImage, setBackImage] = useState<string | undefined | null>(
|
const [backImage, setBackImage] = useState<string | undefined | null>(
|
||||||
undefined
|
undefined
|
||||||
);
|
);
|
||||||
|
const [encodingImage, setEncodingImage] = useState<boolean>(false);
|
||||||
// Movie state
|
|
||||||
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
|
|
||||||
undefined
|
|
||||||
);
|
|
||||||
|
|
||||||
// Network state
|
// Network state
|
||||||
const { data, error, loading } = useFindMovie(id);
|
const { data, error, loading } = useFindMovie(id);
|
||||||
@@ -69,23 +62,7 @@ export const Movie: React.FC = () => {
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
function showImageAlert(imageData: string) {
|
const onImageEncoding = (isEncoding = false) => setEncodingImage(isEncoding);
|
||||||
setImageClipboard(imageData);
|
|
||||||
setIsImageAlertOpen(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setImageFromClipboard(isFrontImage: boolean) {
|
|
||||||
if (isFrontImage) {
|
|
||||||
setFrontImage(imageClipboard);
|
|
||||||
} else {
|
|
||||||
setBackImage(imageClipboard);
|
|
||||||
}
|
|
||||||
|
|
||||||
setImageClipboard(undefined);
|
|
||||||
setIsImageAlertOpen(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
const encodingImage = ImageUtils.usePasteImage(showImageAlert, isEditing);
|
|
||||||
|
|
||||||
if (!isNew && !isEditing) {
|
if (!isNew && !isEditing) {
|
||||||
if (!data || !data.findMovie || loading) return <LoadingIndicator />;
|
if (!data || !data.findMovie || loading) return <LoadingIndicator />;
|
||||||
@@ -99,8 +76,6 @@ export const Movie: React.FC = () => {
|
|||||||
) {
|
) {
|
||||||
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
const ret: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||||
...input,
|
...input,
|
||||||
front_image: frontImage,
|
|
||||||
back_image: backImage,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!isNew) {
|
if (!isNew) {
|
||||||
@@ -174,43 +149,6 @@ export const Movie: React.FC = () => {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderImageAlert() {
|
|
||||||
return (
|
|
||||||
<BSModal
|
|
||||||
show={isImageAlertOpen}
|
|
||||||
onHide={() => setIsImageAlertOpen(false)}
|
|
||||||
>
|
|
||||||
<BSModal.Body>
|
|
||||||
<p>Select image to set</p>
|
|
||||||
</BSModal.Body>
|
|
||||||
<BSModal.Footer>
|
|
||||||
<div>
|
|
||||||
<Button
|
|
||||||
className="mr-2"
|
|
||||||
variant="secondary"
|
|
||||||
onClick={() => setIsImageAlertOpen(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
|
|
||||||
<Button
|
|
||||||
className="mr-2"
|
|
||||||
onClick={() => setImageFromClipboard(false)}
|
|
||||||
>
|
|
||||||
Back Image
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
className="mr-2"
|
|
||||||
onClick={() => setImageFromClipboard(true)}
|
|
||||||
>
|
|
||||||
Front Image
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</BSModal.Footer>
|
|
||||||
</BSModal>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderFrontImage() {
|
function renderFrontImage() {
|
||||||
let image = movie?.front_image_path;
|
let image = movie?.front_image_path;
|
||||||
if (isEditing) {
|
if (isEditing) {
|
||||||
@@ -292,6 +230,7 @@ export const Movie: React.FC = () => {
|
|||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
setFrontImage={setFrontImage}
|
setFrontImage={setFrontImage}
|
||||||
setBackImage={setBackImage}
|
setBackImage={setBackImage}
|
||||||
|
onImageEncoding={onImageEncoding}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -302,7 +241,6 @@ export const Movie: React.FC = () => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{renderDeleteAlert()}
|
{renderDeleteAlert()}
|
||||||
{renderImageAlert()}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -14,7 +14,14 @@ import {
|
|||||||
DurationInput,
|
DurationInput,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { Form, Button, Col, Row, InputGroup } from "react-bootstrap";
|
import {
|
||||||
|
Modal as BSModal,
|
||||||
|
Form,
|
||||||
|
Button,
|
||||||
|
Col,
|
||||||
|
Row,
|
||||||
|
InputGroup,
|
||||||
|
} from "react-bootstrap";
|
||||||
import { DurationUtils, ImageUtils } from "src/utils";
|
import { DurationUtils, ImageUtils } from "src/utils";
|
||||||
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
import { RatingStars } from "src/components/Scenes/SceneDetails/RatingStars";
|
||||||
import { useFormik } from "formik";
|
import { useFormik } from "formik";
|
||||||
@@ -30,6 +37,7 @@ interface IMovieEditPanel {
|
|||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
||||||
@@ -39,12 +47,18 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
setFrontImage,
|
setFrontImage,
|
||||||
setBackImage,
|
setBackImage,
|
||||||
|
onImageEncoding,
|
||||||
}) => {
|
}) => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
|
|
||||||
const isNew = movie === undefined;
|
const isNew = movie === undefined;
|
||||||
|
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [isImageAlertOpen, setIsImageAlertOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
|
const [imageClipboard, setImageClipboard] = useState<string | undefined>(
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
|
||||||
const Scrapers = useListMovieScrapers();
|
const Scrapers = useListMovieScrapers();
|
||||||
const [scrapedMovie, setScrapedMovie] = useState<
|
const [scrapedMovie, setScrapedMovie] = useState<
|
||||||
@@ -70,6 +84,8 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
director: yup.string().optional().nullable(),
|
director: yup.string().optional().nullable(),
|
||||||
synopsis: yup.string().optional().nullable(),
|
synopsis: yup.string().optional().nullable(),
|
||||||
url: yup.string().optional().nullable(),
|
url: yup.string().optional().nullable(),
|
||||||
|
front_image: yup.string().optional().nullable(),
|
||||||
|
back_image: yup.string().optional().nullable(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const initialValues = {
|
const initialValues = {
|
||||||
@@ -77,11 +93,13 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
aliases: movie?.aliases,
|
aliases: movie?.aliases,
|
||||||
duration: movie?.duration,
|
duration: movie?.duration,
|
||||||
date: movie?.date,
|
date: movie?.date,
|
||||||
rating: movie?.rating,
|
rating: movie?.rating ?? null,
|
||||||
studio_id: movie?.studio?.id,
|
studio_id: movie?.studio?.id,
|
||||||
director: movie?.director,
|
director: movie?.director,
|
||||||
synopsis: movie?.synopsis,
|
synopsis: movie?.synopsis,
|
||||||
url: movie?.url,
|
url: movie?.url,
|
||||||
|
front_image: undefined,
|
||||||
|
back_image: undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
type InputValues = typeof initialValues;
|
type InputValues = typeof initialValues;
|
||||||
@@ -92,6 +110,21 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
onSubmit: (values) => onSubmit(getMovieInput(values)),
|
onSubmit: (values) => onSubmit(getMovieInput(values)),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const encodingImage = ImageUtils.usePasteImage(showImageAlert);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setFrontImage(formik.values.front_image);
|
||||||
|
}, [formik.values.front_image, setFrontImage]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setBackImage(formik.values.back_image);
|
||||||
|
}, [formik.values.back_image, setBackImage]);
|
||||||
|
|
||||||
|
useEffect(() => onImageEncoding(encodingImage), [
|
||||||
|
onImageEncoding,
|
||||||
|
encodingImage,
|
||||||
|
]);
|
||||||
|
|
||||||
function setRating(v: number) {
|
function setRating(v: number) {
|
||||||
formik.setFieldValue("rating", v);
|
formik.setFieldValue("rating", v);
|
||||||
}
|
}
|
||||||
@@ -122,6 +155,22 @@ 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) {
|
function getMovieInput(values: InputValues) {
|
||||||
const input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
const input: Partial<GQL.MovieCreateInput | GQL.MovieUpdateInput> = {
|
||||||
...values,
|
...values,
|
||||||
@@ -172,10 +221,10 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image;
|
const imageStr = (state as GQL.ScrapedMovieDataFragment).front_image;
|
||||||
setFrontImage(imageStr ?? undefined);
|
formik.setFieldValue("front_image", imageStr ?? undefined);
|
||||||
|
|
||||||
const backImageStr = (state as GQL.ScrapedMovieDataFragment).back_image;
|
const backImageStr = (state as GQL.ScrapedMovieDataFragment).back_image;
|
||||||
setBackImage(backImageStr ?? undefined);
|
formik.setFieldValue("back_image", backImageStr ?? undefined);
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapeMovieURL() {
|
async function onScrapeMovieURL() {
|
||||||
@@ -256,11 +305,52 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function onFrontImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onFrontImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, setFrontImage);
|
ImageUtils.onImageChange(event, (data) =>
|
||||||
|
formik.setFieldValue("front_image", data)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onBackImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onBackImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
ImageUtils.onImageChange(event, setBackImage);
|
ImageUtils.onImageChange(event, (data) =>
|
||||||
|
formik.setFieldValue("back_image", data)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderImageAlert() {
|
||||||
|
return (
|
||||||
|
<BSModal
|
||||||
|
show={isImageAlertOpen}
|
||||||
|
onHide={() => setIsImageAlertOpen(false)}
|
||||||
|
>
|
||||||
|
<BSModal.Body>
|
||||||
|
<p>Select image to set</p>
|
||||||
|
</BSModal.Body>
|
||||||
|
<BSModal.Footer>
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={() => setIsImageAlertOpen(false)}
|
||||||
|
>
|
||||||
|
Cancel
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
onClick={() => setImageFromClipboard(false)}
|
||||||
|
>
|
||||||
|
Back Image
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
className="mr-2"
|
||||||
|
onClick={() => setImageFromClipboard(true)}
|
||||||
|
>
|
||||||
|
Front Image
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</BSModal.Footer>
|
||||||
|
</BSModal>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
@@ -357,7 +447,9 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
<Col sm={fieldXS} xl={fieldXL}>
|
<Col sm={fieldXS} xl={fieldXL}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={formik.values.rating ?? undefined}
|
value={formik.values.rating ?? undefined}
|
||||||
onSetRating={(value) => formik.setFieldValue("rating", value)}
|
onSetRating={(value) =>
|
||||||
|
formik.setFieldValue("rating", value ?? null)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
@@ -399,20 +491,22 @@ export const MovieEditPanel: React.FC<IMovieEditPanel> = ({
|
|||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
onToggleEdit={onCancel}
|
onToggleEdit={onCancel}
|
||||||
onSave={() => formik.handleSubmit()}
|
onSave={() => formik.handleSubmit()}
|
||||||
|
saveDisabled={!formik.dirty}
|
||||||
onImageChange={onFrontImageChange}
|
onImageChange={onFrontImageChange}
|
||||||
onImageChangeURL={setFrontImage}
|
onImageChangeURL={(i) => formik.setFieldValue("front_image", i)}
|
||||||
onClearImage={() => {
|
onClearImage={() => {
|
||||||
setFrontImage(null);
|
formik.setFieldValue("front_image", null);
|
||||||
}}
|
}}
|
||||||
onBackImageChange={onBackImageChange}
|
onBackImageChange={onBackImageChange}
|
||||||
onBackImageChangeURL={setBackImage}
|
onBackImageChangeURL={(i) => formik.setFieldValue("back_image", i)}
|
||||||
onClearBackImage={() => {
|
onClearBackImage={() => {
|
||||||
setBackImage(null);
|
formik.setFieldValue("back_image", null);
|
||||||
}}
|
}}
|
||||||
onDelete={onDelete}
|
onDelete={onDelete}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{maybeRenderScrapeDialog()}
|
{maybeRenderScrapeDialog()}
|
||||||
|
{renderImageAlert()}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
tag_ids: (performer.tags ?? []).map((t) => t.id),
|
||||||
stash_ids: performer.stash_ids ?? undefined,
|
stash_ids: performer.stash_ids ?? undefined,
|
||||||
image: undefined,
|
image: undefined,
|
||||||
rating: performer.rating ?? undefined,
|
rating: performer.rating ?? null,
|
||||||
details: performer.details ?? "",
|
details: performer.details ?? "",
|
||||||
death_date: performer.death_date ?? "",
|
death_date: performer.death_date ?? "",
|
||||||
hair_color: performer.hair_color ?? "",
|
hair_color: performer.hair_color ?? "",
|
||||||
@@ -691,6 +691,7 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
<Button
|
<Button
|
||||||
className="mr-2"
|
className="mr-2"
|
||||||
variant="primary"
|
variant="primary"
|
||||||
|
disabled={!formik.dirty}
|
||||||
onClick={() => formik.submitForm()}
|
onClick={() => formik.submitForm()}
|
||||||
>
|
>
|
||||||
Save
|
Save
|
||||||
@@ -994,7 +995,9 @@ export const PerformerEditPanel: React.FC<IPerformerDetails> = ({
|
|||||||
<Col xs={fieldXS} xl={fieldXL}>
|
<Col xs={fieldXS} xl={fieldXL}>
|
||||||
<RatingStars
|
<RatingStars
|
||||||
value={formik.values.rating ?? undefined}
|
value={formik.values.rating ?? undefined}
|
||||||
onSetRating={(value) => formik.setFieldValue("rating", value)}
|
onSetRating={(value) =>
|
||||||
|
formik.setFieldValue("rating", value ?? null)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</Col>
|
</Col>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
|
|||||||
@@ -50,7 +50,7 @@ export const Scene: React.FC = () => {
|
|||||||
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
const [timestamp, setTimestamp] = useState<number>(getInitialTimestamp());
|
||||||
const [collapsed, setCollapsed] = useState(false);
|
const [collapsed, setCollapsed] = useState(false);
|
||||||
|
|
||||||
const { data, error, loading } = useFindScene(id);
|
const { data, error, loading, refetch } = useFindScene(id);
|
||||||
const scene = data?.findScene;
|
const scene = data?.findScene;
|
||||||
const {
|
const {
|
||||||
data: sceneStreams,
|
data: sceneStreams,
|
||||||
@@ -505,6 +505,7 @@ export const Scene: React.FC = () => {
|
|||||||
isVisible={activeTabKey === "scene-edit-panel"}
|
isVisible={activeTabKey === "scene-edit-panel"}
|
||||||
scene={scene}
|
scene={scene}
|
||||||
onDelete={() => setIsDeleteAlertOpen(true)}
|
onDelete={() => setIsDeleteAlertOpen(true)}
|
||||||
|
onUpdate={() => refetch()}
|
||||||
/>
|
/>
|
||||||
</Tab.Pane>
|
</Tab.Pane>
|
||||||
</Tab.Content>
|
</Tab.Content>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from "react-bootstrap";
|
} from "react-bootstrap";
|
||||||
import Mousetrap from "mousetrap";
|
import Mousetrap from "mousetrap";
|
||||||
import * as GQL from "src/core/generated-graphql";
|
import * as GQL from "src/core/generated-graphql";
|
||||||
|
import * as yup from "yup";
|
||||||
import {
|
import {
|
||||||
queryScrapeScene,
|
queryScrapeScene,
|
||||||
queryScrapeSceneURL,
|
queryScrapeSceneURL,
|
||||||
@@ -28,9 +29,11 @@ import {
|
|||||||
ImageInput,
|
ImageInput,
|
||||||
} from "src/components/Shared";
|
} from "src/components/Shared";
|
||||||
import { useToast } from "src/hooks";
|
import { useToast } from "src/hooks";
|
||||||
import { ImageUtils, FormUtils, EditableTextUtils, TextUtils } from "src/utils";
|
import { ImageUtils, FormUtils, TextUtils } from "src/utils";
|
||||||
import { MovieSelect } from "src/components/Shared/Select";
|
import { MovieSelect } from "src/components/Shared/Select";
|
||||||
import { SceneMovieTable, MovieSceneIndexMap } from "./SceneMovieTable";
|
import { useFormik } from "formik";
|
||||||
|
import { Prompt } from "react-router";
|
||||||
|
import { SceneMovieTable } from "./SceneMovieTable";
|
||||||
import { RatingStars } from "./RatingStars";
|
import { RatingStars } from "./RatingStars";
|
||||||
import { SceneScrapeDialog } from "./SceneScrapeDialog";
|
import { SceneScrapeDialog } from "./SceneScrapeDialog";
|
||||||
|
|
||||||
@@ -38,6 +41,7 @@ interface IProps {
|
|||||||
scene: GQL.SceneDataFragment;
|
scene: GQL.SceneDataFragment;
|
||||||
isVisible: boolean;
|
isVisible: boolean;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
|
onUpdate?: () => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneEditPanel: React.FC<IProps> = ({
|
export const SceneEditPanel: React.FC<IProps> = ({
|
||||||
@@ -46,37 +50,12 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
onDelete,
|
onDelete,
|
||||||
}) => {
|
}) => {
|
||||||
const Toast = useToast();
|
const Toast = useToast();
|
||||||
const [title, setTitle] = useState<string>(scene.title ?? "");
|
|
||||||
const [details, setDetails] = useState<string>(scene.details ?? "");
|
|
||||||
const [url, setUrl] = useState<string>(scene.url ?? "");
|
|
||||||
const [date, setDate] = useState<string>(scene.date ?? "");
|
|
||||||
const [rating, setRating] = useState<number | undefined>(
|
|
||||||
scene.rating ?? undefined
|
|
||||||
);
|
|
||||||
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
|
||||||
scene.galleries.map((g) => ({
|
scene.galleries.map((g) => ({
|
||||||
id: g.id,
|
id: g.id,
|
||||||
title: g.title ?? TextUtils.fileNameFromPath(g.path ?? ""),
|
title: g.title ?? TextUtils.fileNameFromPath(g.path ?? ""),
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
const [studioId, setStudioId] = useState<string | undefined>(
|
|
||||||
scene.studio?.id
|
|
||||||
);
|
|
||||||
const [performerIds, setPerformerIds] = useState<string[]>(
|
|
||||||
scene.performers.map((p) => p.id)
|
|
||||||
);
|
|
||||||
const [movieIds, setMovieIds] = useState<string[]>(
|
|
||||||
scene.movies.map((m) => m.movie.id)
|
|
||||||
);
|
|
||||||
const [
|
|
||||||
movieSceneIndexes,
|
|
||||||
setMovieSceneIndexes,
|
|
||||||
] = useState<MovieSceneIndexMap>(
|
|
||||||
new Map(scene.movies.map((m) => [m.movie.id, m.scene_index ?? undefined]))
|
|
||||||
);
|
|
||||||
const [tagIds, setTagIds] = useState<string[]>(scene.tags.map((t) => t.id));
|
|
||||||
const [coverImage, setCoverImage] = useState<string>();
|
|
||||||
const [stashIDs, setStashIDs] = useState<GQL.StashIdInput[]>(scene.stash_ids);
|
|
||||||
|
|
||||||
const Scrapers = useListSceneScrapers();
|
const Scrapers = useListSceneScrapers();
|
||||||
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
const [queryableScrapers, setQueryableScrapers] = useState<GQL.Scraper[]>([]);
|
||||||
@@ -94,10 +73,60 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
const [updateScene] = useSceneUpdate();
|
const [updateScene] = useSceneUpdate();
|
||||||
|
|
||||||
|
const schema = yup.object({
|
||||||
|
title: yup.string().optional().nullable(),
|
||||||
|
details: yup.string().optional().nullable(),
|
||||||
|
url: yup.string().optional().nullable(),
|
||||||
|
date: yup.string().optional().nullable(),
|
||||||
|
rating: yup.number().optional().nullable(),
|
||||||
|
gallery_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
|
studio_id: yup.string().optional().nullable(),
|
||||||
|
performer_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
|
movies: yup
|
||||||
|
.object({
|
||||||
|
movie_id: yup.string().required(),
|
||||||
|
scene_index: yup.string().optional().nullable(),
|
||||||
|
})
|
||||||
|
.optional()
|
||||||
|
.nullable(),
|
||||||
|
tag_ids: yup.array(yup.string().required()).optional().nullable(),
|
||||||
|
cover_image: yup.string().optional().nullable(),
|
||||||
|
stash_ids: yup.mixed<GQL.StashIdInput>().optional().nullable(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const initialValues = {
|
||||||
|
title: scene.title ?? "",
|
||||||
|
details: scene.details ?? "",
|
||||||
|
url: scene.url ?? "",
|
||||||
|
date: scene.date ?? "",
|
||||||
|
rating: scene.rating ?? null,
|
||||||
|
gallery_ids: (scene.galleries ?? []).map((g) => g.id),
|
||||||
|
studio_id: scene.studio?.id,
|
||||||
|
performer_ids: (scene.performers ?? []).map((p) => p.id),
|
||||||
|
movies: (scene.movies ?? []).map((m) => {
|
||||||
|
return { movie_id: m.movie.id, scene_index: m.scene_index };
|
||||||
|
}),
|
||||||
|
tag_ids: (scene.tags ?? []).map((t) => t.id),
|
||||||
|
cover_image: undefined,
|
||||||
|
stash_ids: scene.stash_ids ?? undefined,
|
||||||
|
};
|
||||||
|
|
||||||
|
type InputValues = typeof initialValues;
|
||||||
|
|
||||||
|
const formik = useFormik({
|
||||||
|
initialValues,
|
||||||
|
validationSchema: schema,
|
||||||
|
onSubmit: (values) => onSave(getSceneInput(values)),
|
||||||
|
});
|
||||||
|
|
||||||
|
function setRating(v: number) {
|
||||||
|
formik.setFieldValue("rating", v);
|
||||||
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isVisible) {
|
if (isVisible) {
|
||||||
Mousetrap.bind("s s", () => {
|
Mousetrap.bind("s s", () => {
|
||||||
onSave();
|
formik.handleSubmit();
|
||||||
});
|
});
|
||||||
Mousetrap.bind("d d", () => {
|
Mousetrap.bind("d d", () => {
|
||||||
onDelete();
|
onDelete();
|
||||||
@@ -146,86 +175,47 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
setQueryableScrapers(newQueryableScrapers);
|
setQueryableScrapers(newQueryableScrapers);
|
||||||
}, [Scrapers, stashConfig]);
|
}, [Scrapers, stashConfig]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let changed = false;
|
|
||||||
const newMap: MovieSceneIndexMap = new Map();
|
|
||||||
if (movieIds) {
|
|
||||||
movieIds.forEach((id) => {
|
|
||||||
if (!movieSceneIndexes.has(id)) {
|
|
||||||
changed = true;
|
|
||||||
newMap.set(id, undefined);
|
|
||||||
} else {
|
|
||||||
newMap.set(id, movieSceneIndexes.get(id));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!changed) {
|
|
||||||
movieSceneIndexes.forEach((_v, id) => {
|
|
||||||
if (!newMap.has(id)) {
|
|
||||||
// id was removed
|
|
||||||
changed = true;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (changed) {
|
|
||||||
setMovieSceneIndexes(newMap);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [movieIds, movieSceneIndexes]);
|
|
||||||
|
|
||||||
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
const imageEncoding = ImageUtils.usePasteImage(onImageLoad, true);
|
||||||
|
|
||||||
function getSceneInput(): GQL.SceneUpdateInput {
|
function getSceneInput(input: InputValues): GQL.SceneUpdateInput {
|
||||||
return {
|
return {
|
||||||
id: scene.id,
|
id: scene.id,
|
||||||
title,
|
...input,
|
||||||
details,
|
|
||||||
url,
|
|
||||||
date,
|
|
||||||
rating: rating ?? null,
|
|
||||||
gallery_ids: galleries.map((g) => g.id),
|
|
||||||
studio_id: studioId ?? null,
|
|
||||||
performer_ids: performerIds,
|
|
||||||
movies: makeMovieInputs(),
|
|
||||||
tag_ids: tagIds,
|
|
||||||
cover_image: coverImage,
|
|
||||||
stash_ids: stashIDs.map((s) => ({
|
|
||||||
stash_id: s.stash_id,
|
|
||||||
endpoint: s.endpoint,
|
|
||||||
})),
|
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeMovieInputs(): GQL.SceneMovieInput[] | undefined {
|
function setMovieIds(movieIds: string[]) {
|
||||||
if (!movieIds) {
|
const existingMovies = formik.values.movies;
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
|
|
||||||
let ret = movieIds.map((id) => {
|
const newMovies = movieIds.map((m) => {
|
||||||
const r: GQL.SceneMovieInput = {
|
const existing = existingMovies.find((mm) => mm.movie_id === m);
|
||||||
movie_id: id,
|
if (existing) {
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
movie_id: m,
|
||||||
};
|
};
|
||||||
return r;
|
|
||||||
});
|
});
|
||||||
|
|
||||||
ret = ret.map((r) => {
|
formik.setFieldValue("movies", newMovies);
|
||||||
return { scene_index: movieSceneIndexes.get(r.movie_id), ...r };
|
|
||||||
});
|
|
||||||
|
|
||||||
return ret;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onSave() {
|
async function onSave(input: GQL.SceneUpdateInput) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await updateScene({
|
const result = await updateScene({
|
||||||
variables: {
|
variables: {
|
||||||
input: getSceneInput(),
|
input: {
|
||||||
|
...input,
|
||||||
|
rating: input.rating ?? null,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
if (result.data?.sceneUpdate) {
|
if (result.data?.sceneUpdate) {
|
||||||
Toast.success({ content: "Updated scene" });
|
Toast.success({ content: "Updated scene" });
|
||||||
|
// 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);
|
||||||
@@ -234,8 +224,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
const removeStashID = (stashID: GQL.StashIdInput) => {
|
const removeStashID = (stashID: GQL.StashIdInput) => {
|
||||||
setStashIDs(
|
formik.setFieldValue(
|
||||||
stashIDs.filter(
|
"stash_ids",
|
||||||
|
formik.values.stash_ids.filter(
|
||||||
(s) =>
|
(s) =>
|
||||||
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
!(s.endpoint === stashID.endpoint && s.stash_id === stashID.stash_id)
|
||||||
)
|
)
|
||||||
@@ -245,9 +236,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
function renderTableMovies() {
|
function renderTableMovies() {
|
||||||
return (
|
return (
|
||||||
<SceneMovieTable
|
<SceneMovieTable
|
||||||
movieSceneIndexes={movieSceneIndexes}
|
movieScenes={formik.values.movies}
|
||||||
onUpdate={(items) => {
|
onUpdate={(items) => {
|
||||||
setMovieSceneIndexes(items);
|
formik.setFieldValue("movies", items);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -255,7 +246,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
function onImageLoad(imageData: string) {
|
function onImageLoad(imageData: string) {
|
||||||
setCoverImagePreview(imageData);
|
setCoverImagePreview(imageData);
|
||||||
setCoverImage(imageData);
|
formik.setFieldValue("cover_image", imageData);
|
||||||
}
|
}
|
||||||
|
|
||||||
function onCoverImageChange(event: React.FormEvent<HTMLInputElement>) {
|
function onCoverImageChange(event: React.FormEvent<HTMLInputElement>) {
|
||||||
@@ -287,7 +278,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
async function onScrapeClicked(scraper: GQL.Scraper) {
|
async function onScrapeClicked(scraper: GQL.Scraper) {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await queryScrapeScene(scraper.id, getSceneInput());
|
const result = await queryScrapeScene(
|
||||||
|
scraper.id,
|
||||||
|
getSceneInput(formik.values)
|
||||||
|
);
|
||||||
if (!result.data || !result.data.scrapeScene) {
|
if (!result.data || !result.data.scrapeScene) {
|
||||||
Toast.success({
|
Toast.success({
|
||||||
content: "No scenes found",
|
content: "No scenes found",
|
||||||
@@ -328,7 +322,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const currentScene = getSceneInput();
|
const currentScene = getSceneInput(formik.values);
|
||||||
if (!currentScene.cover_image) {
|
if (!currentScene.cover_image) {
|
||||||
currentScene.cover_image = scene.paths.screenshot;
|
currentScene.cover_image = scene.paths.screenshot;
|
||||||
}
|
}
|
||||||
@@ -423,23 +417,23 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
updatedScene: GQL.ScrapedSceneDataFragment
|
updatedScene: GQL.ScrapedSceneDataFragment
|
||||||
) {
|
) {
|
||||||
if (updatedScene.title) {
|
if (updatedScene.title) {
|
||||||
setTitle(updatedScene.title);
|
formik.setFieldValue("title", updatedScene.title);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.details) {
|
if (updatedScene.details) {
|
||||||
setDetails(updatedScene.details);
|
formik.setFieldValue("details", updatedScene.details);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.date) {
|
if (updatedScene.date) {
|
||||||
setDate(updatedScene.date);
|
formik.setFieldValue("date", updatedScene.date);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.url) {
|
if (updatedScene.url) {
|
||||||
setUrl(updatedScene.url);
|
formik.setFieldValue("url", updatedScene.url);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.studio && updatedScene.studio.stored_id) {
|
if (updatedScene.studio && updatedScene.studio.stored_id) {
|
||||||
setStudioId(updatedScene.studio.stored_id);
|
formik.setFieldValue("studio_id", updatedScene.studio.stored_id);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.performers && updatedScene.performers.length > 0) {
|
if (updatedScene.performers && updatedScene.performers.length > 0) {
|
||||||
@@ -449,7 +443,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
if (idPerfs.length > 0) {
|
if (idPerfs.length > 0) {
|
||||||
const newIds = idPerfs.map((p) => p.stored_id);
|
const newIds = idPerfs.map((p) => p.stored_id);
|
||||||
setPerformerIds(newIds as string[]);
|
formik.setFieldValue("performer_ids", newIds as string[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -471,24 +465,24 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
|
|
||||||
if (idTags.length > 0) {
|
if (idTags.length > 0) {
|
||||||
const newIds = idTags.map((p) => p.stored_id);
|
const newIds = idTags.map((p) => p.stored_id);
|
||||||
setTagIds(newIds as string[]);
|
formik.setFieldValue("tag_ids", newIds as string[]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (updatedScene.image) {
|
if (updatedScene.image) {
|
||||||
// image is a base64 string
|
// image is a base64 string
|
||||||
setCoverImage(updatedScene.image);
|
formik.setFieldValue("cover_image", updatedScene.image);
|
||||||
setCoverImagePreview(updatedScene.image);
|
setCoverImagePreview(updatedScene.image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function onScrapeSceneURL() {
|
async function onScrapeSceneURL() {
|
||||||
if (!url) {
|
if (!formik.values.url) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await queryScrapeSceneURL(url);
|
const result = await queryScrapeSceneURL(formik.values.url);
|
||||||
if (!result.data || !result.data.scrapeSceneURL) {
|
if (!result.data || !result.data.scrapeSceneURL) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -501,7 +495,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function maybeRenderScrapeButton() {
|
function maybeRenderScrapeButton() {
|
||||||
if (!url || !urlScrapable(url)) {
|
if (!formik.values.url || !urlScrapable(formik.values.url)) {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -515,219 +509,253 @@ export const SceneEditPanel: React.FC<IProps> = ({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderTextField(field: string, title: string, placeholder?: string) {
|
||||||
|
return (
|
||||||
|
<Form.Group controlId={title} as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title,
|
||||||
|
})}
|
||||||
|
<Col xs={9}>
|
||||||
|
<Form.Control
|
||||||
|
className="text-input"
|
||||||
|
placeholder={placeholder ?? title}
|
||||||
|
{...formik.getFieldProps(field)}
|
||||||
|
isInvalid={!!formik.getFieldMeta(field).error}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
if (isLoading) return <LoadingIndicator />;
|
if (isLoading) return <LoadingIndicator />;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div id="scene-edit-details">
|
<div id="scene-edit-details">
|
||||||
|
<Prompt
|
||||||
|
when={formik.dirty}
|
||||||
|
message="Unsaved changes. Are you sure you want to leave?"
|
||||||
|
/>
|
||||||
|
|
||||||
{maybeRenderScrapeDialog()}
|
{maybeRenderScrapeDialog()}
|
||||||
<div className="form-container row px-3 pt-3">
|
<Form noValidate onSubmit={formik.handleSubmit}>
|
||||||
<div className="col-6 edit-buttons mb-3 pl-0">
|
<div className="form-container row px-3 pt-3">
|
||||||
<Button className="edit-button" variant="primary" onClick={onSave}>
|
<div className="col-6 edit-buttons mb-3 pl-0">
|
||||||
Save
|
<Button
|
||||||
</Button>
|
className="edit-button"
|
||||||
<Button
|
variant="primary"
|
||||||
className="edit-button"
|
disabled={!formik.dirty}
|
||||||
variant="danger"
|
onClick={() => formik.submitForm()}
|
||||||
onClick={() => onDelete()}
|
>
|
||||||
>
|
Save
|
||||||
Delete
|
</Button>
|
||||||
</Button>
|
<Button
|
||||||
|
className="edit-button"
|
||||||
|
variant="danger"
|
||||||
|
onClick={() => onDelete()}
|
||||||
|
>
|
||||||
|
Delete
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<Col xs={6} className="text-right">
|
||||||
|
{maybeRenderStashboxQueryButton()}
|
||||||
|
{renderScraperMenu()}
|
||||||
|
</Col>
|
||||||
</div>
|
</div>
|
||||||
<Col xs={6} className="text-right">
|
<div className="form-container row px-3">
|
||||||
{maybeRenderStashboxQueryButton()}
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
{renderScraperMenu()}
|
{renderTextField("title", "Title")}
|
||||||
</Col>
|
<Form.Group controlId="url" as={Row}>
|
||||||
</div>
|
<Col xs={3} className="pr-0 url-label">
|
||||||
<div className="form-container row px-3">
|
<Form.Label className="col-form-label">URL</Form.Label>
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
<div className="float-right scrape-button-container">
|
||||||
{FormUtils.renderInputGroup({
|
{maybeRenderScrapeButton()}
|
||||||
title: "Title",
|
</div>
|
||||||
value: title,
|
</Col>
|
||||||
onChange: setTitle,
|
<Col xs={9}>
|
||||||
isEditing: true,
|
<Form.Control
|
||||||
})}
|
className="text-input"
|
||||||
<Form.Group controlId="url" as={Row}>
|
placeholder="URL"
|
||||||
<Col xs={3} className="pr-0 url-label">
|
{...formik.getFieldProps("url")}
|
||||||
<Form.Label className="col-form-label">URL</Form.Label>
|
isInvalid={!!formik.getFieldMeta("url").error}
|
||||||
<div className="float-right scrape-button-container">
|
|
||||||
{maybeRenderScrapeButton()}
|
|
||||||
</div>
|
|
||||||
</Col>
|
|
||||||
<Col xs={9}>
|
|
||||||
{EditableTextUtils.renderInputGroup({
|
|
||||||
title: "URL",
|
|
||||||
value: url,
|
|
||||||
onChange: setUrl,
|
|
||||||
isEditing: true,
|
|
||||||
})}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
{FormUtils.renderInputGroup({
|
|
||||||
title: "Date",
|
|
||||||
value: date,
|
|
||||||
isEditing: true,
|
|
||||||
onChange: setDate,
|
|
||||||
placeholder: "YYYY-MM-DD",
|
|
||||||
})}
|
|
||||||
<Form.Group controlId="rating" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: "Rating",
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<RatingStars
|
|
||||||
value={rating}
|
|
||||||
onSetRating={(value) => setRating(value)}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="galleries" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: "Galleries",
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<GallerySelect
|
|
||||||
galleries={galleries}
|
|
||||||
onSelect={(items) => setGalleries(items)}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="studio" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: "Studio",
|
|
||||||
})}
|
|
||||||
<Col xs={9}>
|
|
||||||
<StudioSelect
|
|
||||||
onSelect={(items) =>
|
|
||||||
setStudioId(items.length > 0 ? items[0]?.id : undefined)
|
|
||||||
}
|
|
||||||
ids={studioId ? [studioId] : []}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="performers" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: "Performers",
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<PerformerSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) =>
|
|
||||||
setPerformerIds(items.map((item) => item.id))
|
|
||||||
}
|
|
||||||
ids={performerIds}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="moviesScenes" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: "Movies/Scenes",
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<MovieSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) => setMovieIds(items.map((item) => item.id))}
|
|
||||||
ids={movieIds}
|
|
||||||
/>
|
|
||||||
{renderTableMovies()}
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
|
|
||||||
<Form.Group controlId="tags" as={Row}>
|
|
||||||
{FormUtils.renderLabel({
|
|
||||||
title: "Tags",
|
|
||||||
labelProps: {
|
|
||||||
column: true,
|
|
||||||
sm: 3,
|
|
||||||
xl: 12,
|
|
||||||
},
|
|
||||||
})}
|
|
||||||
<Col sm={9} xl={12}>
|
|
||||||
<TagSelect
|
|
||||||
isMulti
|
|
||||||
onSelect={(items) => setTagIds(items.map((item) => item.id))}
|
|
||||||
ids={tagIds}
|
|
||||||
/>
|
|
||||||
</Col>
|
|
||||||
</Form.Group>
|
|
||||||
<Form.Group controlId="details">
|
|
||||||
<Form.Label>StashIDs</Form.Label>
|
|
||||||
<ul className="pl-0">
|
|
||||||
{stashIDs.map((stashID) => {
|
|
||||||
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
|
||||||
const link = base ? (
|
|
||||||
<a
|
|
||||||
href={`${base}scenes/${stashID.stash_id}`}
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
>
|
|
||||||
{stashID.stash_id}
|
|
||||||
</a>
|
|
||||||
) : (
|
|
||||||
stashID.stash_id
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<li key={stashID.stash_id} className="row no-gutters">
|
|
||||||
<Button
|
|
||||||
variant="danger"
|
|
||||||
className="mr-2 py-0"
|
|
||||||
title="Delete StashID"
|
|
||||||
onClick={() => removeStashID(stashID)}
|
|
||||||
>
|
|
||||||
<Icon icon="trash-alt" />
|
|
||||||
</Button>
|
|
||||||
{link}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</Form.Group>
|
|
||||||
</div>
|
|
||||||
<div className="col-12 col-lg-6 col-xl-12">
|
|
||||||
<Form.Group controlId="details">
|
|
||||||
<Form.Label>Details</Form.Label>
|
|
||||||
<Form.Control
|
|
||||||
as="textarea"
|
|
||||||
className="scene-description text-input"
|
|
||||||
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
|
||||||
setDetails(newValue.currentTarget.value)
|
|
||||||
}
|
|
||||||
value={details}
|
|
||||||
/>
|
|
||||||
</Form.Group>
|
|
||||||
<div>
|
|
||||||
<Form.Group controlId="cover">
|
|
||||||
<Form.Label>Cover Image</Form.Label>
|
|
||||||
{imageEncoding ? (
|
|
||||||
<LoadingIndicator message="Encoding image..." />
|
|
||||||
) : (
|
|
||||||
<img
|
|
||||||
className="scene-cover"
|
|
||||||
src={coverImagePreview}
|
|
||||||
alt="Scene cover"
|
|
||||||
/>
|
/>
|
||||||
)}
|
</Col>
|
||||||
<ImageInput
|
</Form.Group>
|
||||||
isEditing
|
{renderTextField("date", "Date", "YYYY-MM-DD")}
|
||||||
onImageChange={onCoverImageChange}
|
<Form.Group controlId="rating" as={Row}>
|
||||||
onImageURL={onImageLoad}
|
{FormUtils.renderLabel({
|
||||||
/>
|
title: "Rating",
|
||||||
|
})}
|
||||||
|
<Col xs={9}>
|
||||||
|
<RatingStars
|
||||||
|
value={formik.values.rating ?? undefined}
|
||||||
|
onSetRating={(value) =>
|
||||||
|
formik.setFieldValue("rating", value ?? null)
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="galleries" as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title: "Galleries",
|
||||||
|
})}
|
||||||
|
<Col xs={9}>
|
||||||
|
<GallerySelect
|
||||||
|
galleries={galleries}
|
||||||
|
onSelect={(items) => setGalleries(items)}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="studio" as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title: "Studio",
|
||||||
|
})}
|
||||||
|
<Col xs={9}>
|
||||||
|
<StudioSelect
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"studio_id",
|
||||||
|
items.length > 0 ? items[0]?.id : undefined
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.studio_id ? [formik.values.studio_id] : []}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="performers" as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title: "Performers",
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
<Col sm={9} xl={12}>
|
||||||
|
<PerformerSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"performer_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.performer_ids}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="moviesScenes" as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title: "Movies/Scenes",
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
<Col sm={9} xl={12}>
|
||||||
|
<MovieSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
setMovieIds(items.map((item) => item.id))
|
||||||
|
}
|
||||||
|
ids={formik.values.movies.map((m) => m.movie_id)}
|
||||||
|
/>
|
||||||
|
{renderTableMovies()}
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
|
||||||
|
<Form.Group controlId="tags" as={Row}>
|
||||||
|
{FormUtils.renderLabel({
|
||||||
|
title: "Tags",
|
||||||
|
labelProps: {
|
||||||
|
column: true,
|
||||||
|
sm: 3,
|
||||||
|
xl: 12,
|
||||||
|
},
|
||||||
|
})}
|
||||||
|
<Col sm={9} xl={12}>
|
||||||
|
<TagSelect
|
||||||
|
isMulti
|
||||||
|
onSelect={(items) =>
|
||||||
|
formik.setFieldValue(
|
||||||
|
"tag_ids",
|
||||||
|
items.map((item) => item.id)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
ids={formik.values.tag_ids}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Form.Group>
|
||||||
|
<Form.Group controlId="details">
|
||||||
|
<Form.Label>StashIDs</Form.Label>
|
||||||
|
<ul className="pl-0">
|
||||||
|
{formik.values.stash_ids.map((stashID) => {
|
||||||
|
const base = stashID.endpoint.match(/https?:\/\/.*?\//)?.[0];
|
||||||
|
const link = base ? (
|
||||||
|
<a
|
||||||
|
href={`${base}scenes/${stashID.stash_id}`}
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
>
|
||||||
|
{stashID.stash_id}
|
||||||
|
</a>
|
||||||
|
) : (
|
||||||
|
stashID.stash_id
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li key={stashID.stash_id} className="row no-gutters">
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
className="mr-2 py-0"
|
||||||
|
title="Delete StashID"
|
||||||
|
onClick={() => removeStashID(stashID)}
|
||||||
|
>
|
||||||
|
<Icon icon="trash-alt" />
|
||||||
|
</Button>
|
||||||
|
{link}
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
</Form.Group>
|
</Form.Group>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="col-12 col-lg-6 col-xl-12">
|
||||||
|
<Form.Group controlId="details">
|
||||||
|
<Form.Label>Details</Form.Label>
|
||||||
|
<Form.Control
|
||||||
|
as="textarea"
|
||||||
|
className="scene-description text-input"
|
||||||
|
onChange={(newValue: React.ChangeEvent<HTMLTextAreaElement>) =>
|
||||||
|
formik.setFieldValue("details", newValue.currentTarget.value)
|
||||||
|
}
|
||||||
|
value={formik.values.details}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
<div>
|
||||||
|
<Form.Group controlId="cover">
|
||||||
|
<Form.Label>Cover Image</Form.Label>
|
||||||
|
{imageEncoding ? (
|
||||||
|
<LoadingIndicator message="Encoding image..." />
|
||||||
|
) : (
|
||||||
|
<img
|
||||||
|
className="scene-cover"
|
||||||
|
src={coverImagePreview}
|
||||||
|
alt="Scene cover"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<ImageInput
|
||||||
|
isEditing
|
||||||
|
onImageChange={onCoverImageChange}
|
||||||
|
onImageURL={onImageLoad}
|
||||||
|
/>
|
||||||
|
</Form.Group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</Form>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -6,8 +6,8 @@ import { Form, Row, Col } from "react-bootstrap";
|
|||||||
export type MovieSceneIndexMap = Map<string, number | undefined>;
|
export type MovieSceneIndexMap = Map<string, number | undefined>;
|
||||||
|
|
||||||
export interface IProps {
|
export interface IProps {
|
||||||
movieSceneIndexes: MovieSceneIndexMap;
|
movieScenes: GQL.SceneMovieInput[];
|
||||||
onUpdate: (value: MovieSceneIndexMap) => void;
|
onUpdate: (value: GQL.SceneMovieInput[]) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
||||||
@@ -16,40 +16,43 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
|||||||
const { data } = useAllMoviesForFilter();
|
const { data } = useAllMoviesForFilter();
|
||||||
|
|
||||||
const items = !!data && !!data.allMovies ? data.allMovies : [];
|
const items = !!data && !!data.allMovies ? data.allMovies : [];
|
||||||
let itemsFilter: GQL.SlimMovieDataFragment[] = [];
|
|
||||||
|
|
||||||
if (!!props.movieSceneIndexes && !!items) {
|
const movieEntries = props.movieScenes.map((m) => {
|
||||||
props.movieSceneIndexes.forEach((_index, movieId) => {
|
return {
|
||||||
itemsFilter = itemsFilter.concat(items.filter((x) => x.id === movieId));
|
movie: items.find((mm) => m.movie_id === mm.id),
|
||||||
});
|
...m,
|
||||||
}
|
};
|
||||||
|
|
||||||
const storeIdx = itemsFilter.map((movie) => {
|
|
||||||
return props.movieSceneIndexes.get(movie.id);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateFieldChanged = (movieId: string, value: number) => {
|
const updateFieldChanged = (movieId: string, value: number) => {
|
||||||
const newMap = new Map(props.movieSceneIndexes);
|
const newValues = props.movieScenes.map((ms) => {
|
||||||
newMap.set(movieId, value);
|
if (ms.movie_id === movieId) {
|
||||||
props.onUpdate(newMap);
|
return {
|
||||||
|
movie_id: movieId,
|
||||||
|
scene_index: value,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return ms;
|
||||||
|
});
|
||||||
|
props.onUpdate(newValues);
|
||||||
};
|
};
|
||||||
|
|
||||||
function renderTableData() {
|
function renderTableData() {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{itemsFilter!.map((item, index: number) => (
|
{movieEntries.map((m) => (
|
||||||
<Row key={item.toString()}>
|
<Row key={m.movie_id}>
|
||||||
<Form.Label column xs={9}>
|
<Form.Label column xs={9}>
|
||||||
{item.name}
|
{m.movie?.name ?? ""}
|
||||||
</Form.Label>
|
</Form.Label>
|
||||||
<Col xs={3}>
|
<Col xs={3}>
|
||||||
<Form.Control
|
<Form.Control
|
||||||
className="text-input"
|
className="text-input"
|
||||||
type="number"
|
type="number"
|
||||||
value={storeIdx[index] ? storeIdx[index]?.toString() : ""}
|
value={m.scene_index ?? ""}
|
||||||
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
updateFieldChanged(
|
updateFieldChanged(
|
||||||
item.id,
|
m.movie_id,
|
||||||
Number.parseInt(
|
Number.parseInt(
|
||||||
e.currentTarget.value ? e.currentTarget.value : "0",
|
e.currentTarget.value ? e.currentTarget.value : "0",
|
||||||
10
|
10
|
||||||
@@ -64,7 +67,7 @@ export const SceneMovieTable: React.FunctionComponent<IProps> = (
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (props.movieSceneIndexes.size > 0) {
|
if (props.movieScenes.length > 0) {
|
||||||
return (
|
return (
|
||||||
<div className="movie-table">
|
<div className="movie-table">
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
@@ -480,7 +480,7 @@ export const SettingsDLNAPanel: React.FC = () => {
|
|||||||
|
|
||||||
<hr />
|
<hr />
|
||||||
|
|
||||||
<Button variant="primary" type="submit">
|
<Button variant="primary" type="submit" disabled={!dirty}>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</Form>
|
</Form>
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ interface IProps {
|
|||||||
isEditing: boolean;
|
isEditing: boolean;
|
||||||
onToggleEdit: () => void;
|
onToggleEdit: () => void;
|
||||||
onSave: () => void;
|
onSave: () => void;
|
||||||
|
saveDisabled?: boolean;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onAutoTag?: () => void;
|
onAutoTag?: () => void;
|
||||||
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
onImageChange: (event: React.FormEvent<HTMLInputElement>) => void;
|
||||||
@@ -39,7 +40,12 @@ export const DetailsEditNavbar: React.FC<IProps> = (props: IProps) => {
|
|||||||
if (!props.isEditing) return;
|
if (!props.isEditing) return;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button variant="success" className="save" onClick={() => props.onSave()}>
|
<Button
|
||||||
|
variant="success"
|
||||||
|
className="save"
|
||||||
|
disabled={props.saveDisabled}
|
||||||
|
onClick={() => props.onSave()}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user