Support file-less scenes. Add scene split, merge and reassign file (#3006)

* Reassign scene file functionality
* Implement scene create
* Add scene create UI
* Add sceneMerge backend support
* Add merge scene to UI
* Populate split create with scene details
* Add merge button to duplicate checker
* Handle file-less scenes in marker preview generate
* Make unique file name for file-less scene exports
* Add o-counter to scene update input
* Hide rescan for file-less scenes
* Generate heatmap if no speed set on file
* Fix count in scene/image queries
This commit is contained in:
WithoutPants
2022-11-14 16:35:09 +11:00
committed by GitHub
parent d0b0be4dd4
commit 4a054ab081
60 changed files with 2550 additions and 412 deletions

View File

@@ -19,6 +19,7 @@ import {
useSceneUpdate,
mutateReloadScrapers,
queryScrapeSceneQueryFragment,
mutateCreateScene,
} from "src/core/StashService";
import {
PerformerSelect,
@@ -34,7 +35,8 @@ import useToast from "src/hooks/Toast";
import { ImageUtils, FormUtils, getStashIDs } from "src/utils";
import { MovieSelect } from "src/components/Shared/Select";
import { useFormik } from "formik";
import { Prompt } from "react-router-dom";
import { Prompt, useHistory } from "react-router-dom";
import queryString from "query-string";
import { ConfigurationContext } from "src/hooks/Config";
import { stashboxDisplayName } from "src/utils/stashbox";
import { SceneMovieTable } from "./SceneMovieTable";
@@ -50,19 +52,28 @@ const SceneScrapeDialog = lazy(() => import("./SceneScrapeDialog"));
const SceneQueryModal = lazy(() => import("./SceneQueryModal"));
interface IProps {
scene: GQL.SceneDataFragment;
scene: Partial<GQL.SceneDataFragment>;
initialCoverImage?: string;
isNew?: boolean;
isVisible: boolean;
onDelete: () => void;
onUpdate?: () => void;
onDelete?: () => void;
}
export const SceneEditPanel: React.FC<IProps> = ({
scene,
initialCoverImage,
isNew = false,
isVisible,
onDelete,
}) => {
const intl = useIntl();
const Toast = useToast();
const history = useHistory();
const queryParams = queryString.parse(location.search);
const fileID = (queryParams?.file_id ?? "") as string;
const [galleries, setGalleries] = useState<{ id: string; title: string }[]>(
[]
);
@@ -84,15 +95,17 @@ export const SceneEditPanel: React.FC<IProps> = ({
>();
useEffect(() => {
setCoverImagePreview(scene.paths.screenshot ?? undefined);
}, [scene.paths.screenshot]);
setCoverImagePreview(
initialCoverImage ?? scene.paths?.screenshot ?? undefined
);
}, [scene.paths?.screenshot, initialCoverImage]);
useEffect(() => {
setGalleries(
scene.galleries.map((g) => ({
scene.galleries?.map((g) => ({
id: g.id,
title: objectTitle(g),
}))
})) ?? []
);
}, [scene.galleries]);
@@ -142,10 +155,10 @@ export const SceneEditPanel: React.FC<IProps> = ({
return { movie_id: m.movie.id, scene_index: m.scene_index };
}),
tag_ids: (scene.tags ?? []).map((t) => t.id),
cover_image: undefined,
cover_image: initialCoverImage,
stash_ids: getStashIDs(scene.stash_ids),
}),
[scene]
[scene, initialCoverImage]
);
type InputValues = typeof initialValues;
@@ -154,7 +167,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
initialValues,
enableReinitialize: true,
validationSchema: schema,
onSubmit: (values) => onSave(getSceneInput(values)),
onSubmit: (values) => onSave(values),
});
function setRating(v: number) {
@@ -180,7 +193,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
formik.handleSubmit();
});
Mousetrap.bind("d d", () => {
onDelete();
if (onDelete) {
onDelete();
}
});
// numeric keypresses get caught by jwplayer, so blur the element
@@ -234,7 +249,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
function getSceneInput(input: InputValues): GQL.SceneUpdateInput {
return {
id: scene.id,
id: scene.id!,
...input,
};
}
@@ -256,27 +271,49 @@ export const SceneEditPanel: React.FC<IProps> = ({
formik.setFieldValue("movies", newMovies);
}
async function onSave(input: GQL.SceneUpdateInput) {
function getCreateValues(values: InputValues): GQL.SceneCreateInput {
return {
...values,
};
}
async function onSave(input: InputValues) {
setIsLoading(true);
try {
const result = await updateScene({
variables: {
input: {
...input,
rating: input.rating ?? null,
if (!isNew) {
const updateValues = getSceneInput(input);
const result = await updateScene({
variables: {
input: {
...updateValues,
id: scene.id!,
rating: input.rating ?? null,
},
},
},
});
if (result.data?.sceneUpdate) {
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{ entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase() }
),
});
// clear the cover image so that it doesn't appear dirty
formik.resetForm({ values: formik.values });
if (result.data?.sceneUpdate) {
Toast.success({
content: intl.formatMessage(
{ id: "toast.updated_entity" },
{
entity: intl.formatMessage({ id: "scene" }).toLocaleLowerCase(),
}
),
});
}
} else {
const createValues = getCreateValues(input);
const result = await mutateCreateScene({
...createValues,
file_ids: fileID ? [fileID as string] : undefined,
});
if (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) {
Toast.error(e);
}
@@ -316,7 +353,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
async function onScrapeClicked(s: GQL.ScraperSourceInput) {
setIsLoading(true);
try {
const result = await queryScrapeScene(s, scene.id);
const result = await queryScrapeScene(s, scene.id!);
if (!result.data || !result.data.scrapeSingleScene?.length) {
Toast.success({
content: "No scenes found",
@@ -399,7 +436,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
const currentScene = getSceneInput(formik.values);
if (!currentScene.cover_image) {
currentScene.cover_image = scene.paths.screenshot;
currentScene.cover_image = scene.paths!.screenshot;
}
return (
@@ -670,6 +707,24 @@ export const SceneEditPanel: React.FC<IProps> = ({
);
}
const image = useMemo(() => {
if (imageEncoding) {
return <LoadingIndicator message="Encoding image..." />;
}
if (coverImagePreview) {
return (
<img
className="scene-cover"
src={coverImagePreview}
alt={intl.formatMessage({ id: "cover_image" })}
/>
);
}
return <div></div>;
}, [imageEncoding, coverImagePreview, intl]);
if (isLoading) return <LoadingIndicator />;
return (
@@ -687,25 +742,29 @@ export const SceneEditPanel: React.FC<IProps> = ({
<Button
className="edit-button"
variant="primary"
disabled={!formik.dirty}
disabled={!isNew && !formik.dirty}
onClick={() => formik.submitForm()}
>
<FormattedMessage id="actions.save" />
</Button>
<Button
className="edit-button"
variant="danger"
onClick={() => onDelete()}
>
<FormattedMessage id="actions.delete" />
</Button>
</div>
<div className="ml-auto pr-3 text-right d-flex">
<ButtonGroup className="scraper-group">
{renderScraperMenu()}
{renderScrapeQueryMenu()}
</ButtonGroup>
{onDelete && (
<Button
className="edit-button"
variant="danger"
onClick={() => onDelete()}
>
<FormattedMessage id="actions.delete" />
</Button>
)}
</div>
{!isNew && (
<div className="ml-auto pr-3 text-right d-flex">
<ButtonGroup className="scraper-group">
{renderScraperMenu()}
{renderScrapeQueryMenu()}
</ButtonGroup>
</div>
)}
</div>
<div className="form-container row px-3">
<div className="col-12 col-lg-7 col-xl-12">
@@ -758,8 +817,9 @@ export const SceneEditPanel: React.FC<IProps> = ({
})}
<Col sm={9}>
<GallerySelect
galleries={galleries}
selected={galleries}
onSelect={(items) => onSetGalleries(items)}
isMulti
/>
</Col>
</Form.Group>
@@ -918,15 +978,7 @@ export const SceneEditPanel: React.FC<IProps> = ({
<Form.Label>
<FormattedMessage id="cover_image" />
</Form.Label>
{imageEncoding ? (
<LoadingIndicator message="Encoding image..." />
) : (
<img
className="scene-cover"
src={coverImagePreview}
alt={intl.formatMessage({ id: "cover_image" })}
/>
)}
{image}
<ImageInput
isEditing
onImageChange={onCoverImageChange}