mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 12:54:38 +03:00
Generate content for specific scenes (#672)
* Add UI dialog for scene(s) * Move preview preset to config
This commit is contained in:
@@ -3,6 +3,7 @@ import ReactMarkdown from "react-markdown";
|
||||
|
||||
const markup = `
|
||||
### ✨ New Features
|
||||
* Support (re-)generation of generated content for specific scenes.
|
||||
* Add tag thumbnails, tags grid view and tag page.
|
||||
* Add post-scrape dialog.
|
||||
* Add various keyboard shortcuts (see manual).
|
||||
|
||||
@@ -25,6 +25,7 @@ import { AddFilter } from "./AddFilter";
|
||||
interface IListFilterOperation {
|
||||
text: string;
|
||||
onClick: () => void;
|
||||
isDisplayed?: () => boolean;
|
||||
}
|
||||
|
||||
interface IListFilterProps {
|
||||
@@ -363,17 +364,25 @@ export const ListFilter: React.FC<IListFilterProps> = (
|
||||
const options = [renderSelectAll(), renderSelectNone()];
|
||||
|
||||
if (props.otherOperations) {
|
||||
props.otherOperations.forEach((o) => {
|
||||
options.push(
|
||||
<Dropdown.Item
|
||||
key={o.text}
|
||||
className="bg-secondary text-white"
|
||||
onClick={o.onClick}
|
||||
>
|
||||
{o.text}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
});
|
||||
props.otherOperations
|
||||
.filter((o) => {
|
||||
if (!o.isDisplayed) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return o.isDisplayed();
|
||||
})
|
||||
.forEach((o) => {
|
||||
options.push(
|
||||
<Dropdown.Item
|
||||
key={o.text}
|
||||
className="bg-secondary text-white"
|
||||
onClick={o.onClick}
|
||||
>
|
||||
{o.text}
|
||||
</Dropdown.Item>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
if (options.length > 0) {
|
||||
|
||||
@@ -23,6 +23,7 @@ import { SceneDetailPanel } from "./SceneDetailPanel";
|
||||
import { OCounterButton } from "./OCounterButton";
|
||||
import { SceneMoviePanel } from "./SceneMoviePanel";
|
||||
import { DeleteScenesDialog } from "../DeleteScenesDialog";
|
||||
import { SceneGenerateDialog } from "../SceneGenerateDialog";
|
||||
|
||||
export const Scene: React.FC = () => {
|
||||
const { id = "new" } = useParams();
|
||||
@@ -42,6 +43,7 @@ export const Scene: React.FC = () => {
|
||||
const [activeTabKey, setActiveTabKey] = useState("scene-details-panel");
|
||||
|
||||
const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState<boolean>(false);
|
||||
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
|
||||
|
||||
const queryParams = queryString.parse(location.search);
|
||||
const autoplay = queryParams?.autoplay === "true";
|
||||
@@ -134,6 +136,19 @@ export const Scene: React.FC = () => {
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderSceneGenerateDialog() {
|
||||
if (isGenerateDialogOpen && scene) {
|
||||
return (
|
||||
<SceneGenerateDialog
|
||||
selectedIds={[scene.id]}
|
||||
onClose={() => {
|
||||
setIsGenerateDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderOperations() {
|
||||
return (
|
||||
<Dropdown>
|
||||
@@ -146,6 +161,13 @@ export const Scene: React.FC = () => {
|
||||
<Icon icon="ellipsis-v" />
|
||||
</Dropdown.Toggle>
|
||||
<Dropdown.Menu className="bg-secondary text-white">
|
||||
<Dropdown.Item
|
||||
key="generate"
|
||||
className="bg-secondary text-white"
|
||||
onClick={() => setIsGenerateDialogOpen(true)}
|
||||
>
|
||||
Generate...
|
||||
</Dropdown.Item>
|
||||
<Dropdown.Item
|
||||
key="generate-screenshot"
|
||||
className="bg-secondary text-white"
|
||||
@@ -291,6 +313,7 @@ export const Scene: React.FC = () => {
|
||||
|
||||
return (
|
||||
<div className="row">
|
||||
{maybeRenderSceneGenerateDialog()}
|
||||
{maybeRenderDeleteDialog()}
|
||||
<div className="scene-tabs order-xl-first order-last">
|
||||
<div className="d-none d-xl-block">
|
||||
|
||||
105
ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx
Normal file
105
ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx
Normal file
@@ -0,0 +1,105 @@
|
||||
import React, { useState } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||
import { Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
interface ISceneGenerateDialogProps {
|
||||
selectedIds: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const SceneGenerateDialog: React.FC<ISceneGenerateDialogProps> = (
|
||||
props: ISceneGenerateDialogProps
|
||||
) => {
|
||||
const [sprites, setSprites] = useState(true);
|
||||
const [previews, setPreviews] = useState(true);
|
||||
const [markers, setMarkers] = useState(true);
|
||||
const [transcodes, setTranscodes] = useState(false);
|
||||
const [overwrite, setOverwrite] = useState(true);
|
||||
const [imagePreviews, setImagePreviews] = useState(false);
|
||||
|
||||
const Toast = useToast();
|
||||
|
||||
async function onGenerate() {
|
||||
try {
|
||||
await mutateMetadataGenerate({
|
||||
sprites,
|
||||
previews,
|
||||
imagePreviews: previews && imagePreviews,
|
||||
markers,
|
||||
transcodes,
|
||||
thumbnails: false,
|
||||
overwrite,
|
||||
sceneIDs: props.selectedIds,
|
||||
});
|
||||
Toast.success({ content: "Started generating" });
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
props.onClose();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
icon="cogs"
|
||||
header="Generate"
|
||||
accept={{ onClick: onGenerate, text: "Generate" }}
|
||||
cancel={{
|
||||
onClick: () => props.onClose(),
|
||||
text: "Cancel",
|
||||
variant: "secondary",
|
||||
}}
|
||||
>
|
||||
<Form>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="preview-task"
|
||||
checked={previews}
|
||||
label="Previews (video previews which play when hovering over a scene)"
|
||||
onChange={() => setPreviews(!previews)}
|
||||
/>
|
||||
<div className="d-flex flex-row">
|
||||
<div>↳</div>
|
||||
<Form.Check
|
||||
id="image-preview-task"
|
||||
checked={imagePreviews}
|
||||
disabled={!previews}
|
||||
label="Image Previews (animated WebP previews, only required if Preview Type is set to Animated Image)"
|
||||
onChange={() => setImagePreviews(!imagePreviews)}
|
||||
className="ml-2 flex-grow"
|
||||
/>
|
||||
</div>
|
||||
<Form.Check
|
||||
id="sprite-task"
|
||||
checked={sprites}
|
||||
label="Sprites (for the scene scrubber)"
|
||||
onChange={() => setSprites(!sprites)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="marker-task"
|
||||
checked={markers}
|
||||
label="Markers (20 second videos which begin at the given timecode)"
|
||||
onChange={() => setMarkers(!markers)}
|
||||
/>
|
||||
<Form.Check
|
||||
id="transcode-task"
|
||||
checked={transcodes}
|
||||
label="Transcodes (MP4 conversions of unsupported video formats)"
|
||||
onChange={() => setTranscodes(!transcodes)}
|
||||
/>
|
||||
|
||||
<hr />
|
||||
<Form.Check
|
||||
id="overwrite"
|
||||
checked={overwrite}
|
||||
label="Overwrite existing generated files"
|
||||
onChange={() => setOverwrite(!overwrite)}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
@@ -1,4 +1,4 @@
|
||||
import React from "react";
|
||||
import React, { useState } from "react";
|
||||
import _ from "lodash";
|
||||
import { useHistory } from "react-router-dom";
|
||||
import {
|
||||
@@ -9,11 +9,13 @@ import { queryFindScenes } from "src/core/StashService";
|
||||
import { useScenesList } from "src/hooks";
|
||||
import { ListFilterModel } from "src/models/list-filter/filter";
|
||||
import { DisplayMode } from "src/models/list-filter/types";
|
||||
import { showWhenSelected } from "src/hooks/ListHook";
|
||||
import { WallPanel } from "../Wall/WallPanel";
|
||||
import { SceneCard } from "./SceneCard";
|
||||
import { SceneListTable } from "./SceneListTable";
|
||||
import { EditScenesDialog } from "./EditScenesDialog";
|
||||
import { DeleteScenesDialog } from "./DeleteScenesDialog";
|
||||
import { SceneGenerateDialog } from "./SceneGenerateDialog";
|
||||
|
||||
interface ISceneList {
|
||||
subComponent?: boolean;
|
||||
@@ -25,11 +27,18 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
filterHook,
|
||||
}) => {
|
||||
const history = useHistory();
|
||||
const [isGenerateDialogOpen, setIsGenerateDialogOpen] = useState(false);
|
||||
|
||||
const otherOperations = [
|
||||
{
|
||||
text: "Play Random",
|
||||
onClick: playRandom,
|
||||
},
|
||||
{
|
||||
text: "Generate...",
|
||||
onClick: generate,
|
||||
isDisplayed: showWhenSelected,
|
||||
},
|
||||
];
|
||||
|
||||
const addKeybinds = (
|
||||
@@ -82,6 +91,25 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
}
|
||||
}
|
||||
|
||||
async function generate() {
|
||||
setIsGenerateDialogOpen(true);
|
||||
}
|
||||
|
||||
function maybeRenderSceneGenerateDialog(selectedIds: Set<string>) {
|
||||
if (isGenerateDialogOpen) {
|
||||
return (
|
||||
<>
|
||||
<SceneGenerateDialog
|
||||
selectedIds={Array.from(selectedIds.values())}
|
||||
onClose={() => {
|
||||
setIsGenerateDialogOpen(false);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderEditScenesDialog(
|
||||
selectedScenes: SlimSceneDataFragment[],
|
||||
onClose: (applied: boolean) => void
|
||||
@@ -123,7 +151,7 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
);
|
||||
}
|
||||
|
||||
function renderContent(
|
||||
function renderScenes(
|
||||
result: FindScenesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
@@ -149,5 +177,19 @@ export const SceneList: React.FC<ISceneList> = ({
|
||||
}
|
||||
}
|
||||
|
||||
function renderContent(
|
||||
result: FindScenesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>,
|
||||
zoomIndex: number
|
||||
) {
|
||||
return (
|
||||
<>
|
||||
{maybeRenderSceneGenerateDialog(selectedIds)}
|
||||
{renderScenes(result, filter, selectedIds, zoomIndex)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
return listData.template;
|
||||
};
|
||||
|
||||
@@ -17,6 +17,9 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
undefined
|
||||
);
|
||||
const [cachePath, setCachePath] = useState<string | undefined>(undefined);
|
||||
const [previewPreset, setPreviewPreset] = useState<string>(
|
||||
GQL.PreviewPreset.Slow
|
||||
);
|
||||
const [maxTranscodeSize, setMaxTranscodeSize] = useState<
|
||||
GQL.StreamingResolutionEnum | undefined
|
||||
>(undefined);
|
||||
@@ -44,6 +47,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
databasePath,
|
||||
generatedPath,
|
||||
cachePath,
|
||||
previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined,
|
||||
maxTranscodeSize,
|
||||
maxStreamingTranscodeSize,
|
||||
forceMkv,
|
||||
@@ -68,6 +72,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
setDatabasePath(conf.general.databasePath);
|
||||
setGeneratedPath(conf.general.generatedPath);
|
||||
setCachePath(conf.general.cachePath);
|
||||
setPreviewPreset(conf.general.previewPreset);
|
||||
setMaxTranscodeSize(conf.general.maxTranscodeSize ?? undefined);
|
||||
setMaxStreamingTranscodeSize(
|
||||
conf.general.maxStreamingTranscodeSize ?? undefined
|
||||
@@ -274,10 +279,32 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
|
||||
<Form.Group>
|
||||
<h4>Video</h4>
|
||||
<Form.Group id="transcode-size">
|
||||
<h6>Preview encoding preset</h6>
|
||||
<Form.Control
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
value={previewPreset}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setPreviewPreset(e.currentTarget.value)
|
||||
}
|
||||
>
|
||||
{Object.keys(GQL.PreviewPreset).map((p) => (
|
||||
<option value={p.toLowerCase()} key={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
The preset regulates size, quality and encoding time of preview
|
||||
generation. Presets beyond “slow” have diminishing returns and are
|
||||
not recommended.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Group id="transcode-size">
|
||||
<h6>Maximum transcode size</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 input-control"
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setMaxTranscodeSize(translateQuality(event.currentTarget.value))
|
||||
@@ -297,7 +324,7 @@ export const SettingsConfigurationPanel: React.FC = () => {
|
||||
<Form.Group id="streaming-transcode-size">
|
||||
<h6>Maximum streaming transcode size</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 input-control"
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
onChange={(event: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setMaxStreamingTranscodeSize(
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import React, { useState } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { mutateMetadataGenerate } from "src/core/StashService";
|
||||
import { PreviewPreset } from "src/core/generated-graphql";
|
||||
import { useToast } from "src/hooks";
|
||||
|
||||
export const GenerateButton: React.FC = () => {
|
||||
@@ -12,9 +11,6 @@ export const GenerateButton: React.FC = () => {
|
||||
const [transcodes, setTranscodes] = useState(false);
|
||||
const [thumbnails, setThumbnails] = useState(false);
|
||||
const [imagePreviews, setImagePreviews] = useState(false);
|
||||
const [previewPreset, setPreviewPreset] = useState<string>(
|
||||
PreviewPreset.Slow
|
||||
);
|
||||
|
||||
async function onGenerate() {
|
||||
try {
|
||||
@@ -25,7 +21,6 @@ export const GenerateButton: React.FC = () => {
|
||||
markers,
|
||||
transcodes,
|
||||
thumbnails,
|
||||
previewPreset: (previewPreset as PreviewPreset) ?? undefined,
|
||||
});
|
||||
Toast.success({ content: "Started generating" });
|
||||
} catch (e) {
|
||||
@@ -53,31 +48,6 @@ export const GenerateButton: React.FC = () => {
|
||||
className="ml-2 flex-grow"
|
||||
/>
|
||||
</div>
|
||||
<Form.Group controlId="preview-preset" className="mt-2">
|
||||
<Form.Label>
|
||||
<h6>Preview encoding preset</h6>
|
||||
</Form.Label>
|
||||
<Form.Control
|
||||
as="select"
|
||||
value={previewPreset}
|
||||
onChange={(e: React.ChangeEvent<HTMLSelectElement>) =>
|
||||
setPreviewPreset(e.currentTarget.value)
|
||||
}
|
||||
disabled={!previews}
|
||||
className="col-1"
|
||||
>
|
||||
{Object.keys(PreviewPreset).map((p) => (
|
||||
<option value={p.toLowerCase()} key={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
The preset regulates size, quality and encoding time of preview
|
||||
generation. Presets beyond “slow” have diminishing returns and are
|
||||
not recommended.
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
<Form.Check
|
||||
id="sprite-task"
|
||||
checked={sprites}
|
||||
|
||||
@@ -51,6 +51,11 @@ interface IListHookOperation<T> {
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>
|
||||
) => void;
|
||||
isDisplayed?: (
|
||||
result: T,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>
|
||||
) => boolean;
|
||||
}
|
||||
|
||||
interface IListHookOptions<T, E> {
|
||||
@@ -346,6 +351,13 @@ const useList = <QueryResult extends IQueryResult, QueryData extends IDataItem>(
|
||||
onClick: () => {
|
||||
o.onClick(result, filter, selectedIds);
|
||||
},
|
||||
isDisplayed: () => {
|
||||
if (o.isDisplayed) {
|
||||
return o.isDisplayed(result, filter, selectedIds);
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
})
|
||||
: undefined;
|
||||
@@ -594,3 +606,11 @@ export const useTagsList = (
|
||||
getSelectedData: (result: FindTagsQueryResult, selectedIds: Set<string>) =>
|
||||
getSelectedData(result?.data?.findTags?.tags ?? [], selectedIds),
|
||||
});
|
||||
|
||||
export const showWhenSelected = (
|
||||
result: FindScenesQueryResult,
|
||||
filter: ListFilterModel,
|
||||
selectedIds: Set<string>
|
||||
) => {
|
||||
return selectedIds.size > 0;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user