mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
Refactor tasks follow up (#2061)
* Move scan options out of dialog * Move autotag and clean options out of dialogs * Move generate options out of dialog * Animate button while tasks running * Revert to earlier Tasks UI iteration * Rearrange and clarify scan options
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
### ✨ New Features
|
||||
* Refactored tasks page and dialogs and added support for saving default options for scanning, generating and auto-tagging. ([#1949](https://github.com/stashapp/stash/pull/1949))
|
||||
* Save task options when scanning, generating and auto-tagging. ([#1949](https://github.com/stashapp/stash/pull/1949), [#2061](https://github.com/stashapp/stash/pull/2061))
|
||||
* Changed query string parsing behaviour to require all words by default, with the option to `or` keywords and exclude keywords. See the `Browsing` section of the manual for details. ([#1982](https://github.com/stashapp/stash/pull/1982))
|
||||
* Add forward jump 10 second button to video player. ([#1973](https://github.com/stashapp/stash/pull/1973))
|
||||
|
||||
|
||||
@@ -1,262 +0,0 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataAutoTag,
|
||||
useConfiguration,
|
||||
useConfigureDefaults,
|
||||
} from "src/core/StashService";
|
||||
import { Icon, Modal, 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 { DirectorySelectionDialog } from "src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
import { withoutTypename } from "src/utils";
|
||||
|
||||
interface IAutoTagOptions {
|
||||
options: GQL.AutoTagMetadataInput;
|
||||
setOptions: (s: GQL.AutoTagMetadataInput) => void;
|
||||
}
|
||||
|
||||
const AutoTagOptions: React.FC<IAutoTagOptions> = ({
|
||||
options,
|
||||
setOptions: setOptionsState,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { performers, studios, tags } = options;
|
||||
const wildcard = ["*"];
|
||||
|
||||
function toggle(v?: GQL.Maybe<string[]>) {
|
||||
if (!v?.length) {
|
||||
return wildcard;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function setOptions(input: Partial<GQL.AutoTagMetadataInput>) {
|
||||
setOptionsState({ ...options, ...input });
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="autotag-performers"
|
||||
checked={!!performers?.length}
|
||||
label={intl.formatMessage({ id: "performers" })}
|
||||
onChange={() => setOptions({ performers: toggle(performers) })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="autotag-studios"
|
||||
checked={!!studios?.length}
|
||||
label={intl.formatMessage({ id: "studios" })}
|
||||
onChange={() => setOptions({ studios: toggle(studios) })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="autotag-tags"
|
||||
checked={!!tags?.length}
|
||||
label={intl.formatMessage({ id: "tags" })}
|
||||
onChange={() => setOptions({ tags: toggle(tags) })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
interface IAutoTagDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const AutoTagDialog: React.FC<IAutoTagDialogProps> = ({ onClose }) => {
|
||||
const [configureDefaults] = useConfigureDefaults();
|
||||
|
||||
const [options, setOptions] = useState<GQL.AutoTagMetadataInput>({
|
||||
performers: ["*"],
|
||||
studios: ["*"],
|
||||
tags: ["*"],
|
||||
});
|
||||
const [paths, setPaths] = useState<string[]>([]);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
const [settingPaths, setSettingPaths] = useState(false);
|
||||
const [animation, setAnimation] = useState(true);
|
||||
const [savingDefaults, setSavingDefaults] = useState(false);
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const { data: configData, error: configError } = useConfiguration();
|
||||
|
||||
useEffect(() => {
|
||||
if (!configData?.configuration.defaults) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { autoTag } = configData.configuration.defaults;
|
||||
|
||||
if (autoTag) {
|
||||
setOptions(withoutTypename(autoTag));
|
||||
}
|
||||
}, [configData]);
|
||||
|
||||
const selectionStatus = useMemo(() => {
|
||||
const message = paths.length ? (
|
||||
<div>
|
||||
<FormattedMessage id="config.tasks.auto_tag.auto_tagging_paths" />:
|
||||
<ul>
|
||||
{paths.map((p) => (
|
||||
<li key={p}>{p}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
<FormattedMessage id="config.tasks.auto_tag.auto_tagging_all_paths" />.
|
||||
</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>
|
||||
);
|
||||
}, [intl, paths]);
|
||||
|
||||
if (configError) return <div>{configError}</div>;
|
||||
if (!configData) return <div />;
|
||||
|
||||
function makeDefaultAutoTagInput() {
|
||||
const ret = options;
|
||||
const { paths: _paths, ...withoutSpecifics } = ret;
|
||||
return withoutSpecifics;
|
||||
}
|
||||
|
||||
async function onAutoTag() {
|
||||
try {
|
||||
await mutateMetadataAutoTag({
|
||||
...options,
|
||||
paths: paths.length ? paths : undefined,
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.auto_tag" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function onShowManual() {
|
||||
setAnimation(false);
|
||||
setShowManual(true);
|
||||
}
|
||||
|
||||
async function setAsDefault() {
|
||||
try {
|
||||
setSavingDefaults(true);
|
||||
await configureDefaults({
|
||||
variables: {
|
||||
input: {
|
||||
autoTag: makeDefaultAutoTagInput(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.defaults_set" },
|
||||
{ action: intl.formatMessage({ id: "actions.auto_tag" }) }
|
||||
),
|
||||
});
|
||||
} 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="AutoTagging.md"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
modalProps={{ animation }}
|
||||
show
|
||||
icon="cogs"
|
||||
header={intl.formatMessage({ id: "actions.auto_tag" })}
|
||||
accept={{
|
||||
onClick: onAutoTag,
|
||||
text: intl.formatMessage({ id: "actions.auto_tag" }),
|
||||
}}
|
||||
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}
|
||||
<AutoTagOptions options={options} setOptions={(o) => setOptions(o)} />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default AutoTagDialog;
|
||||
@@ -1,198 +0,0 @@
|
||||
import React, { useState, useMemo } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataClean,
|
||||
useConfiguration,
|
||||
// useConfigureDefaults,
|
||||
} from "src/core/StashService";
|
||||
import { Icon, Modal } from "src/components/Shared";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
|
||||
interface ICleanOptions {
|
||||
options: GQL.CleanMetadataInput;
|
||||
setOptions: (s: GQL.CleanMetadataInput) => void;
|
||||
}
|
||||
|
||||
const CleanOptions: React.FC<ICleanOptions> = ({
|
||||
options,
|
||||
setOptions: setOptionsState,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
function setOptions(input: Partial<GQL.CleanMetadataInput>) {
|
||||
setOptionsState({ ...options, ...input });
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="clean-dryrun"
|
||||
checked={options.dryRun}
|
||||
label={intl.formatMessage({ id: "config.tasks.only_dry_run" })}
|
||||
onChange={() => setOptions({ dryRun: !options.dryRun })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
interface ICleanDialog {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const CleanDialog: React.FC<ICleanDialog> = ({ onClose }) => {
|
||||
const [options, setOptions] = useState<GQL.CleanMetadataInput>({
|
||||
dryRun: false,
|
||||
});
|
||||
// TODO - selective clean
|
||||
// const [paths, setPaths] = useState<string[]>([]);
|
||||
// const [settingPaths, setSettingPaths] = useState(false);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
const [animation, setAnimation] = useState(true);
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const { data: configData, error: configError } = useConfiguration();
|
||||
|
||||
const message = useMemo(() => {
|
||||
if (options.dryRun) {
|
||||
return (
|
||||
<p>{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}</p>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<p>
|
||||
{intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
}, [options.dryRun, intl]);
|
||||
|
||||
// const selectionStatus = useMemo(() => {
|
||||
// const message = paths.length ? (
|
||||
// <div>
|
||||
// <FormattedMessage id="config.tasks.auto_tag.auto_tagging_paths" />:
|
||||
// <ul>
|
||||
// {paths.map((p) => (
|
||||
// <li key={p}>{p}</li>
|
||||
// ))}
|
||||
// </ul>
|
||||
// </div>
|
||||
// ) : (
|
||||
// <span>
|
||||
// <FormattedMessage id="config.tasks.auto_tag.auto_tagging_all_paths" />.
|
||||
// </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>
|
||||
// );
|
||||
// }, [intl, paths]);
|
||||
|
||||
if (configError) return <div>{configError}</div>;
|
||||
if (!configData) return <div />;
|
||||
|
||||
async function onClean() {
|
||||
try {
|
||||
await mutateMetadataClean(options);
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.clean" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function onShowManual() {
|
||||
setAnimation(false);
|
||||
setShowManual(true);
|
||||
}
|
||||
|
||||
// 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
|
||||
modalProps={{ animation }}
|
||||
show
|
||||
icon="cogs"
|
||||
header={intl.formatMessage({ id: "actions.clean" })}
|
||||
accept={{
|
||||
onClick: onClean,
|
||||
variant: "danger",
|
||||
text: intl.formatMessage({ id: "actions.clean" }),
|
||||
}}
|
||||
cancel={{
|
||||
onClick: () => onClose(),
|
||||
text: intl.formatMessage({ id: "actions.cancel" }),
|
||||
variant: "secondary",
|
||||
}}
|
||||
leftFooterButtons={
|
||||
<Button
|
||||
title="Help"
|
||||
className="minimal help-button"
|
||||
onClick={() => onShowManual()}
|
||||
>
|
||||
<Icon icon="question-circle" />
|
||||
</Button>
|
||||
}
|
||||
>
|
||||
<Form>
|
||||
<CleanOptions options={options} setOptions={(o) => setOptions(o)} />
|
||||
{message}
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default CleanDialog;
|
||||
@@ -1,5 +1,5 @@
|
||||
import React, { useState, useEffect, useMemo } from "react";
|
||||
import { Form, Button, Collapse } from "react-bootstrap";
|
||||
import { Form, Button } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataGenerate,
|
||||
useConfigureDefaults,
|
||||
@@ -12,284 +12,7 @@ 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>
|
||||
);
|
||||
};
|
||||
import { GenerateOptions } from "../Settings/Tasks/GenerateOptions";
|
||||
|
||||
interface ISceneGenerateDialog {
|
||||
selectedIds?: string[];
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
SCRAPER_PREFIX,
|
||||
STASH_BOX_PREFIX,
|
||||
} from "src/components/Tagger/constants";
|
||||
import { DirectorySelectionDialog } from "src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
||||
import { DirectorySelectionDialog } from "src/components/Settings/Tasks/DirectorySelectionDialog";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
import { IScraperSource } from "./constants";
|
||||
import { OptionsEditor } from "./Options";
|
||||
|
||||
@@ -1,209 +0,0 @@
|
||||
import React, { useState, useMemo, useEffect } from "react";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataScan,
|
||||
useConfigureDefaults,
|
||||
} from "src/core/StashService";
|
||||
import { Icon, Modal, 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 { DirectorySelectionDialog } from "src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog";
|
||||
import { Manual } from "src/components/Help/Manual";
|
||||
import { ScanOptions } from "./Options";
|
||||
import { withoutTypename } from "src/utils";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
|
||||
interface IScanDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export const ScanDialog: React.FC<IScanDialogProps> = ({ onClose }) => {
|
||||
const [configureDefaults] = useConfigureDefaults();
|
||||
|
||||
const [options, setOptions] = useState<GQL.ScanMetadataInput>({});
|
||||
const [paths, setPaths] = useState<string[]>([]);
|
||||
const [showManual, setShowManual] = useState(false);
|
||||
const [settingPaths, setSettingPaths] = useState(false);
|
||||
const [animation, setAnimation] = useState(true);
|
||||
const [savingDefaults, setSavingDefaults] = useState(false);
|
||||
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!configuration?.defaults) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scan } = configuration.defaults;
|
||||
|
||||
if (scan) {
|
||||
setOptions(withoutTypename(scan));
|
||||
}
|
||||
}, [configuration]);
|
||||
|
||||
const selectionStatus = useMemo(() => {
|
||||
const message = paths.length ? (
|
||||
<div>
|
||||
<FormattedMessage id="config.tasks.scan.scanning_paths" />:
|
||||
<ul>
|
||||
{paths.map((p) => (
|
||||
<li key={p}>{p}</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
) : (
|
||||
<span>
|
||||
<FormattedMessage id="config.tasks.scan.scanning_all_paths" />.
|
||||
</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>
|
||||
);
|
||||
}, [intl, paths]);
|
||||
|
||||
if (!configuration) return <div />;
|
||||
|
||||
function makeDefaultScanInput() {
|
||||
const ret = options;
|
||||
const { paths: _paths, ...withoutSpecifics } = ret;
|
||||
return withoutSpecifics;
|
||||
}
|
||||
|
||||
async function onScan() {
|
||||
try {
|
||||
await mutateMetadataScan({
|
||||
...options,
|
||||
paths: paths.length ? paths : undefined,
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.scan" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
function onShowManual() {
|
||||
setAnimation(false);
|
||||
setShowManual(true);
|
||||
}
|
||||
|
||||
async function setAsDefault() {
|
||||
try {
|
||||
setSavingDefaults(true);
|
||||
await configureDefaults({
|
||||
variables: {
|
||||
input: {
|
||||
scan: makeDefaultScanInput(),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.defaults_set" },
|
||||
{ action: intl.formatMessage({ id: "actions.scan" }) }
|
||||
),
|
||||
});
|
||||
} 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
|
||||
modalProps={{ animation, size: "lg" }}
|
||||
show
|
||||
icon="cogs"
|
||||
header={intl.formatMessage({ id: "actions.scan" })}
|
||||
accept={{
|
||||
onClick: onScan,
|
||||
text: intl.formatMessage({ id: "actions.scan" }),
|
||||
}}
|
||||
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}
|
||||
<ScanOptions options={options} setOptions={(o) => setOptions(o)} />
|
||||
</Form>
|
||||
</Modal>
|
||||
);
|
||||
};
|
||||
|
||||
export default ScanDialog;
|
||||
@@ -15,6 +15,7 @@ import { SessionUtils } from "src/utils";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { Manual } from "./Help/Manual";
|
||||
import { SettingsButton } from "./SettingsButton";
|
||||
|
||||
interface IMenuItem {
|
||||
name: string;
|
||||
@@ -262,12 +263,7 @@ export const MainNavbar: React.FC = () => {
|
||||
to="/settings"
|
||||
onClick={handleDismiss}
|
||||
>
|
||||
<Button
|
||||
className="minimal d-flex align-items-center h-100"
|
||||
title="Settings"
|
||||
>
|
||||
<Icon icon="cog" />
|
||||
</Button>
|
||||
<SettingsButton />
|
||||
</NavLink>
|
||||
<Button
|
||||
className="nav-utility minimal"
|
||||
|
||||
@@ -9,7 +9,7 @@ import { SettingsAboutPanel } from "./SettingsAboutPanel";
|
||||
import { SettingsConfigurationPanel } from "./SettingsConfigurationPanel";
|
||||
import { SettingsInterfacePanel } from "./SettingsInterfacePanel/SettingsInterfacePanel";
|
||||
import { SettingsLogsPanel } from "./SettingsLogsPanel";
|
||||
import { SettingsTasksPanel } from "./SettingsTasksPanel/SettingsTasksPanel";
|
||||
import { SettingsTasksPanel } from "./Tasks/SettingsTasksPanel";
|
||||
import { SettingsPluginsPanel } from "./SettingsPluginsPanel";
|
||||
import { SettingsScrapingPanel } from "./SettingsScrapingPanel";
|
||||
import { SettingsToolsPanel } from "./SettingsToolsPanel";
|
||||
|
||||
@@ -1,587 +0,0 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button, ButtonGroup, Card, Form } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataImport,
|
||||
mutateMetadataExport,
|
||||
mutateMigrateHashNaming,
|
||||
usePlugins,
|
||||
mutateRunPluginTask,
|
||||
mutateBackupDatabase,
|
||||
mutateMetadataScan,
|
||||
mutateMetadataIdentify,
|
||||
mutateMetadataAutoTag,
|
||||
mutateMetadataGenerate,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { LoadingIndicator, Modal } from "src/components/Shared";
|
||||
import { downloadFile, withoutTypename } from "src/utils";
|
||||
import IdentifyDialog from "src/components/Dialogs/IdentifyDialog/IdentifyDialog";
|
||||
import { ImportDialog } from "./ImportDialog";
|
||||
import { JobTable } from "./JobTable";
|
||||
import ScanDialog from "src/components/Dialogs/ScanDialog/ScanDialog";
|
||||
import AutoTagDialog from "src/components/Dialogs/AutoTagDialog";
|
||||
import { GenerateDialog } from "src/components/Dialogs/GenerateDialog";
|
||||
import CleanDialog from "src/components/Dialogs/CleanDialog";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { PropsWithChildren } from "react-router/node_modules/@types/react";
|
||||
|
||||
type Plugin = Pick<GQL.Plugin, "id">;
|
||||
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
||||
|
||||
interface ITask {
|
||||
description?: React.ReactNode;
|
||||
}
|
||||
|
||||
const Task: React.FC<PropsWithChildren<ITask>> = ({
|
||||
children,
|
||||
description,
|
||||
}) => (
|
||||
<div className="task">
|
||||
{children}
|
||||
{description ? (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
|
||||
export const SettingsTasksPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [dialogOpen, setDialogOpenState] = useState({
|
||||
importAlert: false,
|
||||
import: false,
|
||||
clean: false,
|
||||
scan: false,
|
||||
autoTag: false,
|
||||
identify: false,
|
||||
generate: false,
|
||||
});
|
||||
|
||||
type DialogOpenState = typeof dialogOpen;
|
||||
|
||||
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
const plugins = usePlugins();
|
||||
|
||||
function setDialogOpen(s: Partial<DialogOpenState>) {
|
||||
setDialogOpenState((v) => {
|
||||
return { ...v, ...s };
|
||||
});
|
||||
}
|
||||
|
||||
async function onImport() {
|
||||
setDialogOpen({ importAlert: false });
|
||||
try {
|
||||
await mutateMetadataImport();
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.import" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImportAlert() {
|
||||
return (
|
||||
<Modal
|
||||
show={dialogOpen.importAlert}
|
||||
icon="trash-alt"
|
||||
accept={{
|
||||
text: intl.formatMessage({ id: "actions.import" }),
|
||||
variant: "danger",
|
||||
onClick: onImport,
|
||||
}}
|
||||
cancel={{ onClick: () => setDialogOpen({ importAlert: false }) }}
|
||||
>
|
||||
<p>{intl.formatMessage({ id: "actions.tasks.import_warning" })}</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function renderCleanDialog() {
|
||||
if (!dialogOpen.clean) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <CleanDialog onClose={() => setDialogOpen({ clean: false })} />;
|
||||
}
|
||||
|
||||
function renderImportDialog() {
|
||||
if (!dialogOpen.import) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <ImportDialog onClose={() => setDialogOpen({ import: false })} />;
|
||||
}
|
||||
|
||||
function renderScanDialog() {
|
||||
if (!dialogOpen.scan) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <ScanDialog onClose={() => setDialogOpen({ scan: false })} />;
|
||||
}
|
||||
|
||||
function renderAutoTagDialog() {
|
||||
if (!dialogOpen.autoTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <AutoTagDialog onClose={() => setDialogOpen({ autoTag: false })} />;
|
||||
}
|
||||
|
||||
function maybeRenderIdentifyDialog() {
|
||||
if (!dialogOpen.identify) return;
|
||||
|
||||
return (
|
||||
<IdentifyDialog onClose={() => setDialogOpen({ identify: false })} />
|
||||
);
|
||||
}
|
||||
|
||||
function maybeRenderGenerateDialog() {
|
||||
if (!dialogOpen.generate) return;
|
||||
|
||||
return (
|
||||
<GenerateDialog onClose={() => setDialogOpen({ generate: false })} />
|
||||
);
|
||||
}
|
||||
|
||||
async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) {
|
||||
await mutateRunPluginTask(plugin.id, operation.name);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: operation.name }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
function renderPluginTasks(plugin: Plugin, pluginTasks: PluginTask[]) {
|
||||
if (!pluginTasks) {
|
||||
return;
|
||||
}
|
||||
|
||||
return pluginTasks.map((o) => {
|
||||
return (
|
||||
<Task description={o.description} key={o.name}>
|
||||
<Button
|
||||
onClick={() => onPluginTaskClicked(plugin, o)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{o.name}
|
||||
</Button>
|
||||
</Task>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function onBackup(download?: boolean) {
|
||||
try {
|
||||
setIsBackupRunning(true);
|
||||
const ret = await mutateBackupDatabase({
|
||||
download,
|
||||
});
|
||||
|
||||
// download the result
|
||||
if (download && ret.data && ret.data.backupDatabase) {
|
||||
const link = ret.data.backupDatabase;
|
||||
downloadFile(link);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setIsBackupRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
function renderPlugins() {
|
||||
if (!plugins.data || !plugins.data.plugins) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskPlugins = plugins.data.plugins.filter(
|
||||
(p) => p.tasks && p.tasks.length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}</h5>
|
||||
{taskPlugins.map((o) => {
|
||||
return (
|
||||
<Form.Group key={`${o.id}`}>
|
||||
<h6>{o.name}</h6>
|
||||
<Card className="task-group">
|
||||
{renderPluginTasks(o, o.tasks ?? [])}
|
||||
</Card>
|
||||
</Form.Group>
|
||||
);
|
||||
})}
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
async function onMigrateHashNaming() {
|
||||
try {
|
||||
await mutateMigrateHashNaming();
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{
|
||||
operation_name: intl.formatMessage({
|
||||
id: "actions.hash_migration",
|
||||
}),
|
||||
}
|
||||
),
|
||||
});
|
||||
} catch (err) {
|
||||
Toast.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function onExport() {
|
||||
try {
|
||||
await mutateMetadataExport();
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.backup" }) }
|
||||
),
|
||||
});
|
||||
} catch (err) {
|
||||
Toast.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function onScanClicked() {
|
||||
// check if defaults are set for scan
|
||||
// if not, then open the dialog
|
||||
if (!configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scan } = configuration?.defaults;
|
||||
if (!scan) {
|
||||
setDialogOpen({ scan: true });
|
||||
} else {
|
||||
mutateMetadataScan(withoutTypename(scan));
|
||||
}
|
||||
}
|
||||
|
||||
async function onIdentifyClicked() {
|
||||
// check if defaults are set for identify
|
||||
// if not, then open the dialog
|
||||
if (!configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { identify } = configuration?.defaults;
|
||||
if (!identify) {
|
||||
setDialogOpen({ identify: true });
|
||||
} else {
|
||||
mutateMetadataIdentify(withoutTypename(identify));
|
||||
}
|
||||
}
|
||||
|
||||
async function onAutoTagClicked() {
|
||||
// check if defaults are set for auto tag
|
||||
// if not, then open the dialog
|
||||
if (!configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { autoTag } = configuration?.defaults;
|
||||
if (!autoTag) {
|
||||
setDialogOpen({ autoTag: true });
|
||||
} else {
|
||||
mutateMetadataAutoTag(withoutTypename(autoTag));
|
||||
}
|
||||
}
|
||||
|
||||
async function onGenerateClicked() {
|
||||
// check if defaults are set for generate
|
||||
// if not, then open the dialog
|
||||
if (!configuration) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { generate } = configuration?.defaults;
|
||||
if (!generate) {
|
||||
setDialogOpen({ generate: true });
|
||||
} else {
|
||||
mutateMetadataGenerate(withoutTypename(generate));
|
||||
}
|
||||
}
|
||||
|
||||
if (isBackupRunning) {
|
||||
return (
|
||||
<LoadingIndicator
|
||||
message={intl.formatMessage({ id: "config.tasks.backing_up_database" })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
{renderImportAlert()}
|
||||
{renderCleanDialog()}
|
||||
{renderImportDialog()}
|
||||
{renderScanDialog()}
|
||||
{renderAutoTagDialog()}
|
||||
{maybeRenderIdentifyDialog()}
|
||||
{maybeRenderGenerateDialog()}
|
||||
|
||||
<h4>{intl.formatMessage({ id: "config.tasks.job_queue" })}</h4>
|
||||
|
||||
<JobTable />
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "library" })}</h5>
|
||||
<Card className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.scan_for_content_desc",
|
||||
})}
|
||||
>
|
||||
<ButtonGroup className="ellipsis-button">
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onScanClicked()}
|
||||
>
|
||||
<FormattedMessage id="actions.scan" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setDialogOpen({ scan: true })}
|
||||
>
|
||||
…
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.identify.description",
|
||||
})}
|
||||
>
|
||||
<ButtonGroup className="ellipsis-button">
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onIdentifyClicked()}
|
||||
>
|
||||
<FormattedMessage id="actions.identify" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setDialogOpen({ identify: true })}
|
||||
>
|
||||
…
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.auto_tag_based_on_filenames",
|
||||
})}
|
||||
>
|
||||
<ButtonGroup className="ellipsis-button">
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onAutoTagClicked()}
|
||||
>
|
||||
<FormattedMessage id="actions.auto_tag" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setDialogOpen({ autoTag: true })}
|
||||
>
|
||||
…
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.cleanup_desc",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ clean: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.clean" />…
|
||||
</Button>
|
||||
</Task>
|
||||
</Card>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.generated_content" })}</h5>
|
||||
|
||||
<Card className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.generate_desc",
|
||||
})}
|
||||
>
|
||||
<ButtonGroup className="ellipsis-button">
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onGenerateClicked()}
|
||||
>
|
||||
<FormattedMessage id="actions.generate" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
onClick={() => setDialogOpen({ generate: true })}
|
||||
>
|
||||
…
|
||||
</Button>
|
||||
</ButtonGroup>
|
||||
</Task>
|
||||
</Card>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "metadata" })}</h5>
|
||||
<Card className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.export_to_json",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="export"
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onExport()}
|
||||
>
|
||||
<FormattedMessage id="actions.full_export" />
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.import_from_exported_json",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="import"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ importAlert: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.full_import" />
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.incremental_import",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="partial-import"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ import: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.import_from_file" />
|
||||
</Button>
|
||||
</Task>
|
||||
</Card>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "actions.backup" })}</h5>
|
||||
<Card className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage(
|
||||
{ id: "config.tasks.backup_database" },
|
||||
{
|
||||
filename_format: (
|
||||
<code>
|
||||
[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]
|
||||
</code>
|
||||
),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
id="backup"
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onBackup()}
|
||||
>
|
||||
<FormattedMessage id="actions.backup" />
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.backup_and_download",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="backupDownload"
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onBackup(true)}
|
||||
>
|
||||
<FormattedMessage id="actions.download_backup" />
|
||||
</Button>
|
||||
</Task>
|
||||
</Card>
|
||||
</Form.Group>
|
||||
|
||||
{renderPlugins()}
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.migrations" })}</h5>
|
||||
|
||||
<Card className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.migrate_hash_files",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="migrateHashNaming"
|
||||
variant="danger"
|
||||
onClick={() => onMigrateHashNaming()}
|
||||
>
|
||||
<FormattedMessage id="actions.rename_gen_files" />
|
||||
</Button>
|
||||
</Task>
|
||||
</Card>
|
||||
</Form.Group>
|
||||
</>
|
||||
);
|
||||
};
|
||||
359
ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx
Normal file
359
ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx
Normal file
@@ -0,0 +1,359 @@
|
||||
import React, { useState } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import {
|
||||
mutateMigrateHashNaming,
|
||||
mutateMetadataExport,
|
||||
mutateBackupDatabase,
|
||||
mutateMetadataImport,
|
||||
mutateMetadataClean,
|
||||
} from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import { downloadFile } from "src/utils";
|
||||
import { Modal } from "../../Shared";
|
||||
import { ImportDialog } from "./ImportDialog";
|
||||
import { Task } from "./Task";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
|
||||
interface ICleanOptions {
|
||||
options: GQL.CleanMetadataInput;
|
||||
setOptions: (s: GQL.CleanMetadataInput) => void;
|
||||
}
|
||||
|
||||
const CleanOptions: React.FC<ICleanOptions> = ({
|
||||
options,
|
||||
setOptions: setOptionsState,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
function setOptions(input: Partial<GQL.CleanMetadataInput>) {
|
||||
setOptionsState({ ...options, ...input });
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="clean-dryrun"
|
||||
checked={options.dryRun}
|
||||
label={intl.formatMessage({ id: "config.tasks.only_dry_run" })}
|
||||
onChange={() => setOptions({ dryRun: !options.dryRun })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
interface IDataManagementTasks {
|
||||
setIsBackupRunning: (v: boolean) => void;
|
||||
}
|
||||
|
||||
export const DataManagementTasks: React.FC<IDataManagementTasks> = ({
|
||||
setIsBackupRunning,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [dialogOpen, setDialogOpenState] = useState({
|
||||
importAlert: false,
|
||||
import: false,
|
||||
clean: false,
|
||||
});
|
||||
|
||||
const [cleanOptions, setCleanOptions] = useState<GQL.CleanMetadataInput>({
|
||||
dryRun: false,
|
||||
});
|
||||
|
||||
type DialogOpenState = typeof dialogOpen;
|
||||
|
||||
function setDialogOpen(s: Partial<DialogOpenState>) {
|
||||
setDialogOpenState((v) => {
|
||||
return { ...v, ...s };
|
||||
});
|
||||
}
|
||||
|
||||
async function onImport() {
|
||||
setDialogOpen({ importAlert: false });
|
||||
try {
|
||||
await mutateMetadataImport();
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.import" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderImportAlert() {
|
||||
return (
|
||||
<Modal
|
||||
show={dialogOpen.importAlert}
|
||||
icon="trash-alt"
|
||||
accept={{
|
||||
text: intl.formatMessage({ id: "actions.import" }),
|
||||
variant: "danger",
|
||||
onClick: onImport,
|
||||
}}
|
||||
cancel={{ onClick: () => setDialogOpen({ importAlert: false }) }}
|
||||
>
|
||||
<p>{intl.formatMessage({ id: "actions.tasks.import_warning" })}</p>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function renderImportDialog() {
|
||||
if (!dialogOpen.import) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <ImportDialog onClose={() => setDialogOpen({ import: false })} />;
|
||||
}
|
||||
|
||||
function renderCleanDialog() {
|
||||
let msg;
|
||||
if (cleanOptions.dryRun) {
|
||||
msg = (
|
||||
<p>{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}</p>
|
||||
);
|
||||
} else {
|
||||
msg = (
|
||||
<p>
|
||||
{intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })}
|
||||
</p>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Modal
|
||||
show={dialogOpen.clean}
|
||||
icon="trash-alt"
|
||||
accept={{
|
||||
text: intl.formatMessage({ id: "actions.clean" }),
|
||||
variant: "danger",
|
||||
onClick: onClean,
|
||||
}}
|
||||
cancel={{ onClick: () => setDialogOpen({ clean: false }) }}
|
||||
>
|
||||
{msg}
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
async function onClean() {
|
||||
try {
|
||||
await mutateMetadataClean(cleanOptions);
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.clean" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setDialogOpen({ clean: false });
|
||||
}
|
||||
}
|
||||
|
||||
async function onMigrateHashNaming() {
|
||||
try {
|
||||
await mutateMigrateHashNaming();
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{
|
||||
operation_name: intl.formatMessage({
|
||||
id: "actions.hash_migration",
|
||||
}),
|
||||
}
|
||||
),
|
||||
});
|
||||
} catch (err) {
|
||||
Toast.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function onExport() {
|
||||
try {
|
||||
await mutateMetadataExport();
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.backup" }) }
|
||||
),
|
||||
});
|
||||
} catch (err) {
|
||||
Toast.error(err);
|
||||
}
|
||||
}
|
||||
|
||||
async function onBackup(download?: boolean) {
|
||||
try {
|
||||
setIsBackupRunning(true);
|
||||
const ret = await mutateBackupDatabase({
|
||||
download,
|
||||
});
|
||||
|
||||
// download the result
|
||||
if (download && ret.data && ret.data.backupDatabase) {
|
||||
const link = ret.data.backupDatabase;
|
||||
downloadFile(link);
|
||||
}
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
} finally {
|
||||
setIsBackupRunning(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
{renderImportAlert()}
|
||||
{renderImportDialog()}
|
||||
{renderCleanDialog()}
|
||||
|
||||
<Form.Group>
|
||||
<div className="task-group">
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.maintenance" })}</h5>
|
||||
<Task
|
||||
headingID="actions.clean"
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.cleanup_desc",
|
||||
})}
|
||||
>
|
||||
<CleanOptions
|
||||
options={cleanOptions}
|
||||
setOptions={(o) => setCleanOptions(o)}
|
||||
/>
|
||||
<Button
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ clean: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.clean" />…
|
||||
</Button>
|
||||
</Task>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "metadata" })}</h5>
|
||||
<div className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.export_to_json",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="export"
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onExport()}
|
||||
>
|
||||
<FormattedMessage id="actions.full_export" />
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.import_from_exported_json",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="import"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ importAlert: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.full_import" />
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.incremental_import",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="partial-import"
|
||||
variant="danger"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ import: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.import_from_file" />
|
||||
</Button>
|
||||
</Task>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "actions.backup" })}</h5>
|
||||
<div className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage(
|
||||
{ id: "config.tasks.backup_database" },
|
||||
{
|
||||
filename_format: (
|
||||
<code>
|
||||
[origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS]
|
||||
</code>
|
||||
),
|
||||
}
|
||||
)}
|
||||
>
|
||||
<Button
|
||||
id="backup"
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onBackup()}
|
||||
>
|
||||
<FormattedMessage id="actions.backup" />
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.backup_and_download",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="backupDownload"
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onBackup(true)}
|
||||
>
|
||||
<FormattedMessage id="actions.download_backup" />
|
||||
</Button>
|
||||
</Task>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.migrations" })}</h5>
|
||||
|
||||
<div className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.migrate_hash_files",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
id="migrateHashNaming"
|
||||
variant="danger"
|
||||
onClick={() => onMigrateHashNaming()}
|
||||
>
|
||||
<FormattedMessage id="actions.rename_gen_files" />
|
||||
</Button>
|
||||
</Task>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
283
ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx
Normal file
283
ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx
Normal file
@@ -0,0 +1,283 @@
|
||||
import React, { useState } from "react";
|
||||
import { Form, Button, Collapse } from "react-bootstrap";
|
||||
import { Icon } from "src/components/Shared";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
interface IGenerateOptions {
|
||||
options: GQL.GenerateMetadataInput;
|
||||
setOptions: (s: GQL.GenerateMetadataInput) => void;
|
||||
}
|
||||
|
||||
export 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>
|
||||
);
|
||||
};
|
||||
384
ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx
Normal file
384
ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx
Normal file
@@ -0,0 +1,384 @@
|
||||
import React, { useState, useEffect } from "react";
|
||||
import { FormattedMessage, useIntl } from "react-intl";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import {
|
||||
mutateMetadataScan,
|
||||
mutateMetadataAutoTag,
|
||||
mutateMetadataGenerate,
|
||||
useConfigureDefaults,
|
||||
} from "src/core/StashService";
|
||||
import { withoutTypename } from "src/utils";
|
||||
import { ConfigurationContext } from "src/hooks/Config";
|
||||
import { IdentifyDialog } from "../../Dialogs/IdentifyDialog/IdentifyDialog";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { DirectorySelectionDialog } from "./DirectorySelectionDialog";
|
||||
import { ScanOptions } from "./ScanOptions";
|
||||
import { useToast } from "src/hooks";
|
||||
import { GenerateOptions } from "./GenerateOptions";
|
||||
import { Task } from "./Task";
|
||||
|
||||
interface IAutoTagOptions {
|
||||
options: GQL.AutoTagMetadataInput;
|
||||
setOptions: (s: GQL.AutoTagMetadataInput) => void;
|
||||
}
|
||||
|
||||
const AutoTagOptions: React.FC<IAutoTagOptions> = ({
|
||||
options,
|
||||
setOptions: setOptionsState,
|
||||
}) => {
|
||||
const intl = useIntl();
|
||||
|
||||
const { performers, studios, tags } = options;
|
||||
const wildcard = ["*"];
|
||||
|
||||
function toggle(v?: GQL.Maybe<string[]>) {
|
||||
if (!v?.length) {
|
||||
return wildcard;
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function setOptions(input: Partial<GQL.AutoTagMetadataInput>) {
|
||||
setOptionsState({ ...options, ...input });
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="autotag-performers"
|
||||
checked={!!performers?.length}
|
||||
label={intl.formatMessage({ id: "performers" })}
|
||||
onChange={() => setOptions({ performers: toggle(performers) })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="autotag-studios"
|
||||
checked={!!studios?.length}
|
||||
label={intl.formatMessage({ id: "studios" })}
|
||||
onChange={() => setOptions({ studios: toggle(studios) })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="autotag-tags"
|
||||
checked={!!tags?.length}
|
||||
label={intl.formatMessage({ id: "tags" })}
|
||||
onChange={() => setOptions({ tags: toggle(tags) })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
|
||||
export const LibraryTasks: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
const [configureDefaults] = useConfigureDefaults();
|
||||
|
||||
const [dialogOpen, setDialogOpenState] = useState({
|
||||
clean: false,
|
||||
scan: false,
|
||||
autoTag: false,
|
||||
identify: false,
|
||||
});
|
||||
|
||||
const [scanOptions, setScanOptions] = useState<GQL.ScanMetadataInput>({});
|
||||
const [
|
||||
autoTagOptions,
|
||||
setAutoTagOptions,
|
||||
] = useState<GQL.AutoTagMetadataInput>({
|
||||
performers: ["*"],
|
||||
studios: ["*"],
|
||||
tags: ["*"],
|
||||
});
|
||||
|
||||
function getDefaultGenerateOptions(): GQL.GenerateMetadataInput {
|
||||
return {
|
||||
sprites: true,
|
||||
phashes: true,
|
||||
previews: true,
|
||||
markers: true,
|
||||
previewOptions: {
|
||||
previewSegments: 0,
|
||||
previewSegmentDuration: 0,
|
||||
previewPreset: GQL.PreviewPreset.Slow,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const [
|
||||
generateOptions,
|
||||
setGenerateOptions,
|
||||
] = useState<GQL.GenerateMetadataInput>(getDefaultGenerateOptions());
|
||||
|
||||
type DialogOpenState = typeof dialogOpen;
|
||||
|
||||
const { configuration } = React.useContext(ConfigurationContext);
|
||||
|
||||
useEffect(() => {
|
||||
if (!configuration?.defaults) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { scan, autoTag } = configuration.defaults;
|
||||
|
||||
if (scan) {
|
||||
setScanOptions(withoutTypename(scan));
|
||||
}
|
||||
if (autoTag) {
|
||||
setAutoTagOptions(withoutTypename(autoTag));
|
||||
}
|
||||
|
||||
if (configuration?.defaults.generate) {
|
||||
const { generate } = configuration.defaults;
|
||||
setGenerateOptions(withoutTypename(generate));
|
||||
} else if (configuration?.general) {
|
||||
// backwards compatibility
|
||||
const { general } = configuration;
|
||||
setGenerateOptions((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,
|
||||
},
|
||||
}));
|
||||
}
|
||||
}, [configuration]);
|
||||
|
||||
function setDialogOpen(s: Partial<DialogOpenState>) {
|
||||
setDialogOpenState((v) => {
|
||||
return { ...v, ...s };
|
||||
});
|
||||
}
|
||||
|
||||
function renderScanDialog() {
|
||||
if (!dialogOpen.scan) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <DirectorySelectionDialog onClose={onScanDialogClosed} />;
|
||||
}
|
||||
|
||||
function onScanDialogClosed(paths?: string[]) {
|
||||
if (paths) {
|
||||
runScan(paths);
|
||||
}
|
||||
|
||||
setDialogOpen({ scan: false });
|
||||
}
|
||||
|
||||
async function runScan(paths?: string[]) {
|
||||
try {
|
||||
configureDefaults({
|
||||
variables: {
|
||||
input: {
|
||||
scan: scanOptions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await mutateMetadataScan({
|
||||
...scanOptions,
|
||||
paths,
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.scan" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function renderAutoTagDialog() {
|
||||
if (!dialogOpen.autoTag) {
|
||||
return;
|
||||
}
|
||||
|
||||
return <DirectorySelectionDialog onClose={onAutoTagDialogClosed} />;
|
||||
}
|
||||
|
||||
function onAutoTagDialogClosed(paths?: string[]) {
|
||||
if (paths) {
|
||||
runAutoTag(paths);
|
||||
}
|
||||
|
||||
setDialogOpen({ autoTag: false });
|
||||
}
|
||||
|
||||
async function runAutoTag(paths?: string[]) {
|
||||
try {
|
||||
configureDefaults({
|
||||
variables: {
|
||||
input: {
|
||||
autoTag: autoTagOptions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await mutateMetadataAutoTag({
|
||||
...autoTagOptions,
|
||||
paths,
|
||||
});
|
||||
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.auto_tag" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
function maybeRenderIdentifyDialog() {
|
||||
if (!dialogOpen.identify) return;
|
||||
|
||||
return (
|
||||
<IdentifyDialog onClose={() => setDialogOpen({ identify: false })} />
|
||||
);
|
||||
}
|
||||
|
||||
async function onGenerateClicked() {
|
||||
try {
|
||||
configureDefaults({
|
||||
variables: {
|
||||
input: {
|
||||
generate: generateOptions,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await mutateMetadataGenerate(generateOptions);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: intl.formatMessage({ id: "actions.generate" }) }
|
||||
),
|
||||
});
|
||||
} catch (e) {
|
||||
Toast.error(e);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
{renderScanDialog()}
|
||||
{renderAutoTagDialog()}
|
||||
{maybeRenderIdentifyDialog()}
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "library" })}</h5>
|
||||
|
||||
<div className="task-group">
|
||||
<Task
|
||||
headingID="actions.scan"
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.scan_for_content_desc",
|
||||
})}
|
||||
>
|
||||
<ScanOptions options={scanOptions} setOptions={setScanOptions} />
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
onClick={() => runScan()}
|
||||
>
|
||||
<FormattedMessage id="actions.scan" />
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
onClick={() => setDialogOpen({ scan: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.selective_scan" />…
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
headingID="config.tasks.identify.heading"
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.identify.description",
|
||||
})}
|
||||
>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ identify: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.identify" />…
|
||||
</Button>
|
||||
</Task>
|
||||
|
||||
<Task
|
||||
headingID="config.tasks.auto_tagging"
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.auto_tag_based_on_filenames",
|
||||
})}
|
||||
>
|
||||
<AutoTagOptions
|
||||
options={autoTagOptions}
|
||||
setOptions={(o) => setAutoTagOptions(o)}
|
||||
/>
|
||||
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
className="mr-2"
|
||||
onClick={() => runAutoTag()}
|
||||
>
|
||||
<FormattedMessage id="actions.auto_tag" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => setDialogOpen({ autoTag: true })}
|
||||
>
|
||||
<FormattedMessage id="actions.selective_auto_tag" />…
|
||||
</Button>
|
||||
</Task>
|
||||
</div>
|
||||
</Form.Group>
|
||||
|
||||
<hr />
|
||||
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.generated_content" })}</h5>
|
||||
|
||||
<div className="task-group">
|
||||
<Task
|
||||
description={intl.formatMessage({
|
||||
id: "config.tasks.generate_desc",
|
||||
})}
|
||||
>
|
||||
<GenerateOptions
|
||||
options={generateOptions}
|
||||
setOptions={setGenerateOptions}
|
||||
/>
|
||||
<Button
|
||||
variant="secondary"
|
||||
type="submit"
|
||||
onClick={() => onGenerateClicked()}
|
||||
>
|
||||
<FormattedMessage id="actions.generate" />
|
||||
</Button>
|
||||
</Task>
|
||||
</div>
|
||||
</Form.Group>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
75
ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx
Normal file
75
ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx
Normal file
@@ -0,0 +1,75 @@
|
||||
import React from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { Button, Form } from "react-bootstrap";
|
||||
import { mutateRunPluginTask, usePlugins } from "src/core/StashService";
|
||||
import { useToast } from "src/hooks";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { Task } from "./Task";
|
||||
|
||||
type Plugin = Pick<GQL.Plugin, "id">;
|
||||
type PluginTask = Pick<GQL.PluginTask, "name" | "description">;
|
||||
|
||||
export const PluginTasks: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const Toast = useToast();
|
||||
|
||||
const plugins = usePlugins();
|
||||
|
||||
function renderPlugins() {
|
||||
if (!plugins.data || !plugins.data.plugins) {
|
||||
return;
|
||||
}
|
||||
|
||||
const taskPlugins = plugins.data.plugins.filter(
|
||||
(p) => p.tasks && p.tasks.length > 0
|
||||
);
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<h5>{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}</h5>
|
||||
{taskPlugins.map((o) => {
|
||||
return (
|
||||
<Form.Group key={`${o.id}`}>
|
||||
<h6>{o.name}</h6>
|
||||
<div className="task-group">
|
||||
{renderPluginTasks(o, o.tasks ?? [])}
|
||||
</div>
|
||||
</Form.Group>
|
||||
);
|
||||
})}
|
||||
</Form.Group>
|
||||
);
|
||||
}
|
||||
|
||||
function renderPluginTasks(plugin: Plugin, pluginTasks: PluginTask[]) {
|
||||
if (!pluginTasks) {
|
||||
return;
|
||||
}
|
||||
|
||||
return pluginTasks.map((o) => {
|
||||
return (
|
||||
<Task description={o.description} key={o.name}>
|
||||
<Button
|
||||
onClick={() => onPluginTaskClicked(plugin, o)}
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
>
|
||||
{o.name}
|
||||
</Button>
|
||||
</Task>
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) {
|
||||
await mutateRunPluginTask(plugin.id, operation.name);
|
||||
Toast.success({
|
||||
content: intl.formatMessage(
|
||||
{ id: "config.tasks.added_job_to_queue" },
|
||||
{ operation_name: operation.name }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
return <Form.Group>{renderPlugins()}</Form.Group>;
|
||||
};
|
||||
@@ -30,22 +30,6 @@ export const ScanOptions: React.FC<IScanOptions> = ({
|
||||
|
||||
return (
|
||||
<Form.Group>
|
||||
<Form.Check
|
||||
id="use-file-metadata"
|
||||
checked={useFileMetadata ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "config.tasks.set_name_date_details_from_metadata_if_present",
|
||||
})}
|
||||
onChange={() => setOptions({ useFileMetadata: !useFileMetadata })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="strip-file-extension"
|
||||
checked={stripFileExtension ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "config.tasks.dont_include_file_extension_as_part_of_the_title",
|
||||
})}
|
||||
onChange={() => setOptions({ stripFileExtension: !stripFileExtension })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="scan-generate-previews"
|
||||
checked={scanGeneratePreviews ?? false}
|
||||
@@ -103,6 +87,22 @@ export const ScanOptions: React.FC<IScanOptions> = ({
|
||||
setOptions({ scanGenerateThumbnails: !scanGenerateThumbnails })
|
||||
}
|
||||
/>
|
||||
<Form.Check
|
||||
id="strip-file-extension"
|
||||
checked={stripFileExtension ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "config.tasks.dont_include_file_extension_as_part_of_the_title",
|
||||
})}
|
||||
onChange={() => setOptions({ stripFileExtension: !stripFileExtension })}
|
||||
/>
|
||||
<Form.Check
|
||||
id="use-file-metadata"
|
||||
checked={useFileMetadata ?? false}
|
||||
label={intl.formatMessage({
|
||||
id: "config.tasks.set_name_date_details_from_metadata_if_present",
|
||||
})}
|
||||
onChange={() => setOptions({ useFileMetadata: !useFileMetadata })}
|
||||
/>
|
||||
</Form.Group>
|
||||
);
|
||||
};
|
||||
36
ui/v2.5/src/components/Settings/Tasks/SettingsTasksPanel.tsx
Normal file
36
ui/v2.5/src/components/Settings/Tasks/SettingsTasksPanel.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
import React, { useState } from "react";
|
||||
import { useIntl } from "react-intl";
|
||||
import { LoadingIndicator } from "src/components/Shared";
|
||||
import { LibraryTasks } from "./LibraryTasks";
|
||||
import { DataManagementTasks } from "./DataManagementTasks";
|
||||
import { PluginTasks } from "./PluginTasks";
|
||||
import { JobTable } from "./JobTable";
|
||||
|
||||
export const SettingsTasksPanel: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const [isBackupRunning, setIsBackupRunning] = useState<boolean>(false);
|
||||
|
||||
if (isBackupRunning) {
|
||||
return (
|
||||
<LoadingIndicator
|
||||
message={intl.formatMessage({ id: "config.tasks.backing_up_database" })}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<h4>{intl.formatMessage({ id: "config.tasks.job_queue" })}</h4>
|
||||
|
||||
<JobTable />
|
||||
|
||||
<hr />
|
||||
|
||||
<LibraryTasks />
|
||||
<hr />
|
||||
<DataManagementTasks setIsBackupRunning={setIsBackupRunning} />
|
||||
<hr />
|
||||
<PluginTasks />
|
||||
</>
|
||||
);
|
||||
};
|
||||
26
ui/v2.5/src/components/Settings/Tasks/Task.tsx
Normal file
26
ui/v2.5/src/components/Settings/Tasks/Task.tsx
Normal file
@@ -0,0 +1,26 @@
|
||||
import React, { PropsWithChildren } from "react";
|
||||
import { Form } from "react-bootstrap";
|
||||
import { FormattedMessage } from "react-intl";
|
||||
|
||||
interface ITask {
|
||||
headingID?: string;
|
||||
description?: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Task: React.FC<PropsWithChildren<ITask>> = ({
|
||||
children,
|
||||
headingID,
|
||||
description,
|
||||
}) => (
|
||||
<div className="task">
|
||||
{headingID ? (
|
||||
<h6>
|
||||
<FormattedMessage id={headingID} />
|
||||
</h6>
|
||||
) : undefined}
|
||||
{children}
|
||||
{description ? (
|
||||
<Form.Text className="text-muted">{description}</Form.Text>
|
||||
) : undefined}
|
||||
</div>
|
||||
);
|
||||
@@ -162,28 +162,20 @@
|
||||
}
|
||||
}
|
||||
|
||||
.card.task-group {
|
||||
padding-bottom: 0.5rem;
|
||||
padding-top: 0.5rem;
|
||||
|
||||
.task {
|
||||
padding-bottom: 0.5rem;
|
||||
.task-group {
|
||||
padding-top: 0.5rem;
|
||||
|
||||
&:not(:last-child) {
|
||||
border-bottom: 1px solid $dark-gray2;
|
||||
}
|
||||
}
|
||||
padding-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.ellipsis-button {
|
||||
.btn:first-child {
|
||||
border-right: 1px solid $card-bg;
|
||||
.task {
|
||||
&:not(:first-child) {
|
||||
padding-top: 0.5rem;
|
||||
}
|
||||
|
||||
.btn:last-child {
|
||||
border-left: 1px solid $card-bg;
|
||||
padding-left: 0.25rem;
|
||||
padding-right: 0.25rem;
|
||||
&:not(:last-child) {
|
||||
padding-bottom: 1rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
65
ui/v2.5/src/components/SettingsButton.tsx
Normal file
65
ui/v2.5/src/components/SettingsButton.tsx
Normal file
@@ -0,0 +1,65 @@
|
||||
import { FontAwesomeIcon } from "@fortawesome/react-fontawesome";
|
||||
import React, { useEffect, useState } from "react";
|
||||
import { Button } from "react-bootstrap";
|
||||
import { useJobQueue, useJobsSubscribe } from "src/core/StashService";
|
||||
import * as GQL from "src/core/generated-graphql";
|
||||
import { useIntl } from "react-intl";
|
||||
|
||||
type JobFragment = Pick<
|
||||
GQL.Job,
|
||||
"id" | "status" | "subTasks" | "description" | "progress"
|
||||
>;
|
||||
|
||||
export const SettingsButton: React.FC = () => {
|
||||
const intl = useIntl();
|
||||
const jobStatus = useJobQueue();
|
||||
const jobsSubscribe = useJobsSubscribe();
|
||||
|
||||
const [queue, setQueue] = useState<JobFragment[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
setQueue(jobStatus.data?.jobQueue ?? []);
|
||||
}, [jobStatus]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!jobsSubscribe.data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const event = jobsSubscribe.data.jobsSubscribe;
|
||||
|
||||
function updateJob() {
|
||||
setQueue((q) =>
|
||||
q.map((j) => {
|
||||
if (j.id === event.job.id) {
|
||||
return event.job;
|
||||
}
|
||||
|
||||
return j;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
switch (event.type) {
|
||||
case GQL.JobStatusUpdateType.Add:
|
||||
// add to the end of the queue
|
||||
setQueue((q) => q.concat([event.job]));
|
||||
break;
|
||||
case GQL.JobStatusUpdateType.Remove:
|
||||
setQueue((q) => q.filter((j) => j.id !== event.job.id));
|
||||
break;
|
||||
case GQL.JobStatusUpdateType.Update:
|
||||
updateJob();
|
||||
break;
|
||||
}
|
||||
}, [jobsSubscribe.data]);
|
||||
|
||||
return (
|
||||
<Button
|
||||
className="minimal d-flex align-items-center h-100"
|
||||
title={intl.formatMessage({ id: "settings" })}
|
||||
>
|
||||
<FontAwesomeIcon icon="cog" spin={queue.length > 0} />
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
@@ -78,6 +78,8 @@
|
||||
"select_all": "Select All",
|
||||
"select_folders": "Select folders",
|
||||
"select_none": "Select None",
|
||||
"selective_auto_tag": "Selective Auto Tag",
|
||||
"selective_scan": "Selective Scan",
|
||||
"set_as_default": "Set as default",
|
||||
"set_back_image": "Back image…",
|
||||
"set_front_image": "Front image…",
|
||||
@@ -299,6 +301,7 @@
|
||||
"backup_and_download": "Performs a backup of the database and downloads the resulting file.",
|
||||
"backup_database": "Performs a backup of the database to the same directory as the database, with the filename format {filename_format}",
|
||||
"cleanup_desc": "Check for missing files and remove them from the database. This is a destructive action.",
|
||||
"data_management": "Data management",
|
||||
"defaults_set": "Defaults have been set and will be used when clicking the {action} button on the Tasks page.",
|
||||
"dont_include_file_extension_as_part_of_the_title": "Don't include file extension as part of the title",
|
||||
"export_to_json": "Exports the database content into JSON format in the metadata directory.",
|
||||
@@ -307,7 +310,7 @@
|
||||
"generating_from_paths": "Generating for scenes from the following paths"
|
||||
},
|
||||
"generate_desc": "Generate supporting image, sprite, video, vtt and other files.",
|
||||
"generate_phashes_during_scan": "Generate phashes during scan (for deduplication and scene identification)",
|
||||
"generate_phashes_during_scan": "Generate perceptual hashes during scan (for deduplication and scene identification)",
|
||||
"generate_previews_during_scan": "Generate image previews during scan (animated WebP previews, only required if Preview Type is set to Animated Image)",
|
||||
"generate_sprites_during_scan": "Generate sprites during scan (for the scene scrubber)",
|
||||
"generate_thumbnails_during_scan": "Generate thumbnails for images during scan.",
|
||||
@@ -346,7 +349,7 @@
|
||||
"scanning_all_paths": "Scanning all paths"
|
||||
},
|
||||
"scan_for_content_desc": "Scan for new content and add it to the database.",
|
||||
"set_name_date_details_from_metadata_if_present": "Set name, date, details from metadata (if present)"
|
||||
"set_name_date_details_from_metadata_if_present": "Set name, date, details from embedded file metadata (if present)"
|
||||
},
|
||||
"tools": {
|
||||
"scene_duplicate_checker": "Scene Duplicate Checker",
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
"select_all": "Seleccionar todo",
|
||||
"select_folders": "Seleccionar carpetas",
|
||||
"select_none": "Deseleccionar todo",
|
||||
"selective_auto_tag": "Etiquetado automático selectivo",
|
||||
"selective_scan": "Búsqueda selectiva",
|
||||
"set_as_default": "Establecer por defecto",
|
||||
"set_back_image": "Contraportada…",
|
||||
"set_front_image": "Portada…",
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
"select_all": "Seleziona Tutto",
|
||||
"select_folders": "Seleziona cartelle",
|
||||
"select_none": "Deseleziona Tutto",
|
||||
"selective_auto_tag": "Tag Automatico Selettivo",
|
||||
"selective_scan": "Scansione Selettiva",
|
||||
"set_as_default": "Imposta come Predefinito",
|
||||
"set_back_image": "Immagine Retro…",
|
||||
"set_front_image": "Immagine Frontale…",
|
||||
|
||||
@@ -64,6 +64,8 @@
|
||||
"search": "Buscar",
|
||||
"select_all": "Selecionar todos",
|
||||
"select_none": "Selecionar nenhum",
|
||||
"selective_auto_tag": "Auto Tag seletivo",
|
||||
"selective_scan": "Escaneamento seletivo",
|
||||
"set_as_default": "Aplicar como padrão",
|
||||
"set_back_image": "Imagem de fundo…",
|
||||
"set_front_image": "Imagem frontal…",
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
"select_all": "Välj alla",
|
||||
"select_folders": "Välj mappar",
|
||||
"select_none": "Välj inga",
|
||||
"selective_auto_tag": "Selektiv Auto Tag",
|
||||
"selective_scan": "Selektiv skanning",
|
||||
"set_as_default": "Välj som standard",
|
||||
"set_back_image": "Bakbild…",
|
||||
"set_front_image": "Frambild…",
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
"select_all": "选择所有",
|
||||
"select_folders": "选择目录",
|
||||
"select_none": "清除选择",
|
||||
"selective_auto_tag": "选择性自动生成标签",
|
||||
"selective_scan": "选择性扫描",
|
||||
"set_as_default": "设置为默认",
|
||||
"set_back_image": "设置背面图…",
|
||||
"set_front_image": "设置正面图…",
|
||||
|
||||
@@ -79,6 +79,8 @@
|
||||
"select_all": "全選",
|
||||
"select_folders": "選擇資料夾",
|
||||
"select_none": "清除選擇",
|
||||
"selective_auto_tag": "選擇性套用標籤",
|
||||
"selective_scan": "選擇性掃描",
|
||||
"set_as_default": "設為預設",
|
||||
"set_back_image": "設定背面圖…",
|
||||
"set_front_image": "設定正面圖…",
|
||||
|
||||
Reference in New Issue
Block a user