Files
stash/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx
WithoutPants d176e9f192 Settings UI refactor (#2086)
* Full width settings page
* Group settings
* Make config fields optional
* auto save on change
* Add settings context
* Refactor stash library section
* Restructure settings
* Refactor tasks page
* Add collapse buttons for setting groups
* Add collapse buttons in library
* Add loading indicator
* Simplify task options. Add details to manual
* Add manual links to tasks page
* Add help tooltips
* Refactor about page
* Refactor log page
* Refactor tools panel
* Refactor plugin page
* Refactor task queue
* Improve disabled styling
2021-12-14 15:06:05 +11:00

419 lines
11 KiB
TypeScript

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 { SettingSection } from "../SettingSection";
import { BooleanSetting, Setting, SettingGroup } from "../Inputs";
import { ManualLink } from "src/components/Help/Manual";
import { Icon } from "src/components/Shared";
interface IAutoTagOptions {
options: GQL.AutoTagMetadataInput;
setOptions: (s: GQL.AutoTagMetadataInput) => void;
}
const AutoTagOptions: React.FC<IAutoTagOptions> = ({
options,
setOptions: setOptionsState,
}) => {
const { performers, studios, tags } = options;
const wildcard = ["*"];
function set(v?: boolean) {
if (v) {
return wildcard;
}
return [];
}
function setOptions(input: Partial<GQL.AutoTagMetadataInput>) {
setOptionsState({ ...options, ...input });
}
return (
<>
<BooleanSetting
id="autotag-performers"
checked={!!performers?.length}
headingID="performers"
onChange={(v) => setOptions({ performers: set(v) })}
/>
<BooleanSetting
id="autotag-studios"
checked={!!studios?.length}
headingID="studios"
onChange={(v) => setOptions({ studios: set(v) })}
/>
<BooleanSetting
id="autotag-tags"
checked={!!tags?.length}
headingID="tags"
onChange={(v) => setOptions({ tags: set(v) })}
/>
</>
);
};
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()}
<SettingSection headingID="library">
<SettingGroup
settingProps={{
heading: (
<>
<FormattedMessage id="actions.scan" />
<ManualLink tab="Tasks">
<Icon icon="question-circle" />
</ManualLink>
</>
),
subHeadingID: "config.tasks.scan_for_content_desc",
}}
topLevel={
<>
<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>
</>
}
collapsible
>
<ScanOptions options={scanOptions} setOptions={setScanOptions} />
</SettingGroup>
</SettingSection>
<SettingSection>
<Setting
heading={
<>
<FormattedMessage id="config.tasks.identify.heading" />
<ManualLink tab="Identify">
<Icon icon="question-circle" />
</ManualLink>
</>
}
subHeadingID="config.tasks.identify.description"
>
<Button
variant="secondary"
type="submit"
onClick={() => setDialogOpen({ identify: true })}
>
<FormattedMessage id="actions.identify" />
</Button>
</Setting>
</SettingSection>
<SettingSection>
<SettingGroup
settingProps={{
heading: (
<>
<FormattedMessage id="actions.auto_tag" />
<ManualLink tab="AutoTagging">
<Icon icon="question-circle" />
</ManualLink>
</>
),
subHeadingID: "config.tasks.auto_tag_based_on_filenames",
}}
topLevel={
<>
<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>
</>
}
collapsible
>
<AutoTagOptions
options={autoTagOptions}
setOptions={(o) => setAutoTagOptions(o)}
/>
</SettingGroup>
</SettingSection>
<SettingSection headingID="config.tasks.generated_content">
<SettingGroup
settingProps={{
heading: (
<>
<FormattedMessage id="actions.generate" />
<ManualLink tab="Tasks">
<Icon icon="question-circle" />
</ManualLink>
</>
),
subHeadingID: "config.tasks.generate_desc",
}}
topLevel={
<Button
variant="secondary"
type="submit"
onClick={() => onGenerateClicked()}
>
<FormattedMessage id="actions.generate" />
</Button>
}
collapsible
>
<GenerateOptions
options={generateOptions}
setOptions={setGenerateOptions}
/>
</SettingGroup>
</SettingSection>
</Form.Group>
);
};