mirror of
https://github.com/stashapp/stash.git
synced 2025-12-18 04:44:37 +03:00
Tasks page refactor (#1949)
* Add scan dialog * Add Auto Tag dialog * Refactor and combine Generate dialog * Add clean dialog * Support scan task default setting * Support saving auto tag defaults * Support for generate defaults
This commit is contained in:
557
ui/v2.5/src/components/Dialogs/GenerateDialog.tsx
Normal file
557
ui/v2.5/src/components/Dialogs/GenerateDialog.tsx
Normal file
@@ -0,0 +1,557 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Form, Button, Collapse } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataGenerate,
|
||||
useConfigureDefaults,
|
||||
} from "src/core/StashService";
|
||||
import { Modal, Icon, OperationButton } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
// import { DirectorySelectionDialog } from "../Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
||||
import { Manual } from "../Help/Manual";
|
||||
import { withoutTypename } from "src/utils";
|
||||
|
||||
interface IGenerateOptions {
|
||||
options: GQL.GenerateMetadataInput;
|
||||
setOptions: (s: GQL.GenerateMetadataInput) => void;
|
||||
}
|
||||
|
||||
const GenerateOptions: React.FC<IGenerateOptions> = ({
|
||||
options,
|
||||
setOptions: setOptionsState,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false);
|
||||
|
||||
const previewOptions: GQL.GeneratePreviewOptionsInput =
|
||||
options.previewOptions ?? {};
|
||||
|
||||
function setOptions(input: Partial<GQL.GenerateMetadataInput>) {
|
||||
setOptionsState({ ...options, ...input });
|
||||
}
|
||||
|
||||
function setPreviewOptions(input: Partial<GQL.GeneratePreviewOptionsInput>) {
|
||||
setOptions({
|
||||
previewOptions: {
|
||||
...previewOptions,
|
||||
...input,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="preview-task"
|
||||
checked={options.previews ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "dialogs.scene_gen.video_previews",
|
||||
})}
|
||||
onChange={() => setOptions({ previews: !options.previews })}
|
||||
/>
|
||||
<div className="d-flex flex-row">
|
||||
<div>↳</div>
|
||||
<Form.Check
|
||||
id="image-preview-task"
|
||||
checked={options.imagePreviews ?? false}
|
||||
disabled={!options.previews}
|
||||
label={intl.formatMessage({
|
||||
id: "dialogs.scene_gen.image_previews",
|
||||
})}
|
||||
onChange={() =>
|
||||
setOptions({ imagePreviews: !options.imagePreviews })
|
||||
}
|
||||
className="ml-2 flex-grow"
|
||||
/>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Button
|
||||
onClick={() => setPreviewOptionsOpen(!previewOptionsOpen)}
|
||||
className="minimal pl-0 no-focus"
|
||||
>
|
||||
<Icon icon={previewOptionsOpen ? "chevron-down" : "chevron-right"} />
|
||||
<span>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_options",
|
||||
})}
|
||||
</span>
|
||||
</Button>
|
||||
<Form.Group>
|
||||
<Collapse in={previewOptionsOpen}>
|
||||
<Form.Group className="mt-2">
|
||||
<Form.Group id="preview-preset">
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_preset_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="w-auto input-control"
|
||||
as="select"
|
||||
value={previewOptions.previewPreset ?? GQL.PreviewPreset.Slow}
|
||||
onChange={(e) =>
|
||||
setPreviewOptions({
|
||||
previewPreset: e.currentTarget.value as GQL.PreviewPreset,
|
||||
})
|
||||
}
|
||||
>
|
||||
{Object.keys(GQL.PreviewPreset).map((p) => (
|
||||
<option value={p.toLowerCase()} key={p}>
|
||||
{p}
|
||||
</option>
|
||||
))}
|
||||
</Form.Control>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_preset_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="preview-segments">
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_seg_count_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
value={previewOptions.previewSegments?.toString() ?? ""}
|
||||
onChange={(e) =>
|
||||
setPreviewOptions({
|
||||
previewSegments: Number.parseInt(
|
||||
e.currentTarget.value,
|
||||
10
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_seg_count_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="preview-segment-duration">
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_seg_duration_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
type="number"
|
||||
value={
|
||||
previewOptions.previewSegmentDuration?.toString() ?? ""
|
||||
}
|
||||
onChange={(e) =>
|
||||
setPreviewOptions({
|
||||
previewSegmentDuration: Number.parseFloat(
|
||||
e.currentTarget.value
|
||||
),
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_seg_duration_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="preview-exclude-start">
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_start_time_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
value={previewOptions.previewExcludeStart ?? ""}
|
||||
onChange={(e) =>
|
||||
setPreviewOptions({
|
||||
previewExcludeStart: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_start_time_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group id="preview-exclude-start">
|
||||
<h6>
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_end_time_head",
|
||||
})}
|
||||
</h6>
|
||||
<Form.Control
|
||||
className="col col-sm-6 text-input"
|
||||
value={previewOptions.previewExcludeEnd ?? ""}
|
||||
onChange={(e) =>
|
||||
setPreviewOptions({
|
||||
previewExcludeEnd: e.currentTarget.value,
|
||||
})
|
||||
}
|
||||
/>
|
||||
<Form.Text className="text-muted">
|
||||
{intl.formatMessage({
|
||||
id: "dialogs.scene_gen.preview_exclude_end_time_desc",
|
||||
})}
|
||||
</Form.Text>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
</Collapse>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="sprite-task"
|
||||
checked={options.sprites ?? false}
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.sprites" })}
|
||||
onChange={() => setOptions({ sprites: !options.sprites })}
|
||||
/>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="marker-task"
|
||||
checked={options.markers ?? false}
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.markers" })}
|
||||
onChange={() => setOptions({ markers: !options.markers })}
|
||||
/>
|
||||
<div className="d-flex flex-row">
|
||||
<div>↳</div>
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="marker-image-preview-task"
|
||||
checked={options.markerImagePreviews ?? false}
|
||||
disabled={!options.markers}
|
||||
label={intl.formatMessage({
|
||||
id: "dialogs.scene_gen.marker_image_previews",
|
||||
})}
|
||||
onChange={() =>
|
||||
setOptions({
|
||||
markerImagePreviews: !options.markerImagePreviews,
|
||||
})
|
||||
}
|
||||
className="ml-2 flex-grow"
|
||||
/>
|
||||
<Form.Check
|
||||
id="marker-screenshot-task"
|
||||
checked={options.markerScreenshots ?? false}
|
||||
disabled={!options.markers}
|
||||
label={intl.formatMessage({
|
||||
id: "dialogs.scene_gen.marker_screenshots",
|
||||
})}
|
||||
onChange={() =>
|
||||
setOptions({ markerScreenshots: !options.markerScreenshots })
|
||||
}
|
||||
className="ml-2 flex-grow"
|
||||
/>
|
||||
</Form.Group>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="transcode-task"
|
||||
checked={options.transcodes ?? false}
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.transcodes" })}
|
||||
onChange={() => setOptions({ transcodes: !options.transcodes })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="phash-task"
|
||||
checked={options.phashes ?? false}
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.phash" })}
|
||||
onChange={() => setOptions({ phashes: !options.phashes })}
|
||||
/>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="overwrite"
|
||||
checked={options.overwrite ?? false}
|
||||
label={intl.formatMessage({ id: "dialogs.scene_gen.overwrite" })}
|
||||
onChange={() => setOptions({ overwrite: !options.overwrite })}
|
||||
/>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
interface ISceneGenerateDialog {
|
||||
selectedIds?: string[];
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const GenerateDialog: React.FC<ISceneGenerateDialog> = ({
|
||||
selectedIds,
|
||||
onClose,
|
||||
}) => {
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
const [configureDefaults] = useConfigureDefaults();
|
||||
|
||||
function getDefaultOptions(): GQL.GenerateMetadataInput {
|
||||
return {
|
||||
sprites: true,
|
||||
phashes: true,
|
||||
previews: true,
|
||||
markers: true,
|
||||
previewOptions: {
|
||||
previewSegments: 0,
|
||||
previewSegmentDuration: 0,
|
||||
previewPreset: GQL.PreviewPreset.Slow,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [options, setOptions] = useState<GQL.GenerateMetadataInput>(
|
||||
getDefaultOptions()
|
||||
);
|
||||
const [configRead, setConfigRead] = useState(false);
|
||||
const [paths /* , setPaths */] = useState<string[]>([]);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
// const [settingPaths, setSettingPaths] = useState(false);
|
||||
const [savingDefaults, setSavingDefaults] = useState(false);
|
||||
const [animation, setAnimation] = useState(true);
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
if (configRead) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (configuration?.defaults.generate) {
|
||||
const { generate } = configuration.defaults;
|
||||
setOptions(withoutTypename(generate));
|
||||
setConfigRead(true);
|
||||
} else if (configuration?.general) {
|
||||
// backwards compatibility
|
||||
const { general } = configuration;
|
||||
setOptions((existing) => ({
|
||||
...existing,
|
||||
previewOptions: {
|
||||
...existing.previewOptions,
|
||||
previewSegments:
|
||||
general.previewSegments ?? existing.previewOptions?.previewSegments,
|
||||
previewSegmentDuration:
|
||||
general.previewSegmentDuration ??
|
||||
existing.previewOptions?.previewSegmentDuration,
|
||||
previewExcludeStart:
|
||||
general.previewExcludeStart ??
|
||||
existing.previewOptions?.previewExcludeStart,
|
||||
previewExcludeEnd:
|
||||
general.previewExcludeEnd ??
|
||||
existing.previewOptions?.previewExcludeEnd,
|
||||
previewPreset:
|
||||
general.previewPreset ?? existing.previewOptions?.previewPreset,
|
||||
},
|
||||
}));
|
||||
setConfigRead(true);
|
||||
}
|
||||
}, [configuration, configRead]);
|
||||
|
||||
const selectionStatus = useMemo(() => {
|
||||
if (selectedIds) {
|
||||
return (
|
||||
<Form.Group id="selected-generate-ids">
|
||||
<FormattedMessage
|
||||
id="config.tasks.generate.generating_scenes"
|
||||
values={{
|
||||
num: selectedIds.length,
|
||||
scene: intl.formatMessage(
|
||||
{
|
||||
id: "countables.scenes",
|
||||
},
|
||||
{
|
||||
count: selectedIds.length,
|
||||
}
|
||||
),
|
||||
}}
|
||||
/>
|
||||
.
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
const message = paths.length ? (
|
||||
<div>
|
||||
<FormattedMessage id="config.tasks.generate.generating_from_paths" />:
|
||||
<ul>
|
||||
{paths.map((p) => (
|
||||
<li key={p}>{p}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
<FormattedMessage
|
||||
id="config.tasks.generate.generating_scenes"
|
||||
values={{
|
||||
num: intl.formatMessage({ id: "all" }),
|
||||
scene: intl.formatMessage(
|
||||
{
|
||||
id: "countables.scenes",
|
||||
},
|
||||
{
|
||||
count: 0,
|
||||
}
|
||||
),
|
||||
}}
|
||||
/>
|
||||
.
|
||||
</span>
|
||||
);
|
||||
|
||||
// function onClick() {
|
||||
// setAnimation(false);
|
||||
// setSettingPaths(true);
|
||||
// }
|
||||
|
||||
return (
|
||||
<Form.Group className="dialog-selected-folders">
|
||||
<div>
|
||||
{message}
|
||||
{/* <div>
|
||||
<Button
|
||||
title={intl.formatMessage({ id: "actions.select_folders" })}
|
||||
onClick={() => onClick()}
|
||||
>
|
||||
<Icon icon="folder-open" />
|
||||
</Button>
|
||||
</div> */}
|
||||
</div>
|
||||
</Form.Group>
|
||||
);
|
||||
}, [selectedIds, intl, paths]);
|
||||
|
||||
async function onGenerate() {
|
||||
try {
|
||||
await mutateMetadataGenerate(options);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.generate" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function makeDefaultGenerateInput() {
|
||||
const ret = options;
|
||||
// const { paths: _paths, ...withoutSpecifics } = ret;
|
||||
const { overwrite: _overwrite, ...withoutSpecifics } = ret;
|
||||
return withoutSpecifics;
|
||||
}
|
||||
|
||||
function onShowManual() {
|
||||
setAnimation(false);
|
||||
setShowManual(true);
|
||||
}
|
||||
|
||||
async function setAsDefault() {
|
||||
try {
|
||||
setSavingDefaults(true);
|
||||
await configureDefaults({
|
||||
variables: {
|
||||
input: {
|
||||
generate: makeDefaultGenerateInput(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.defaults_set" },
|
||||
{ action: intl.formatMessage({ id: "actions.generate" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setSavingDefaults(false);
|
||||
}
|
||||
}
|
||||
|
||||
// if (settingPaths) {
|
||||
// return (
|
||||
// <DirectorySelectionDialog
|
||||
// animation={false}
|
||||
// allowEmpty
|
||||
// initialPaths={paths}
|
||||
// onClose={(p) => {
|
||||
// if (p) {
|
||||
// setPaths(p);
|
||||
// }
|
||||
// setSettingPaths(false);
|
||||
// }}
|
||||
// />
|
||||
// );
|
||||
// }
|
||||
|
||||
if (showManual) {
|
||||
return (
|
||||
<Manual
|
||||
animation={false}
|
||||
show
|
||||
onClose={() => setShowManual(false)}
|
||||
defaultActiveTab="Tasks.md"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show
|
||||
modalProps={{ animation, size: "lg" }}
|
||||
icon="cogs"
|
||||
header={intl.formatMessage({ id: "actions.generate" })}
|
||||
accept={{
|
||||
onClick: onGenerate,
|
||||
text: intl.formatMessage({ id: "actions.generate" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => onClose(),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
disabled={savingDefaults}
|
||||
footerButtons={
|
||||
<OperationButton variant="secondary" operation={setAsDefault}>
|
||||
<FormattedMessage id="actions.set_as_default" />
|
||||
</OperationButton>
|
||||
}
|
||||
leftFooterButtons={
|
||||
<Button
|
||||
title="Help"
|
||||
className="minimal help-button"
|
||||
onClick={() => onShowManual()}
|
||||
>
|
||||
<Icon icon="question-circle" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form>
|
||||
{selectionStatus}
|
||||
<GenerateOptions options={options} setOptions={setOptions} />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
Reference in New Issue
Block a user