mirror of
https://github.com/stashapp/stash.git
synced 2025-12-17 20:34:37 +03:00
* 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
419 lines
11 KiB
TypeScript
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>
|
|
);
|
|
};
|