From ff32f22c1a3e8def5df9a5d5e4c3ed747f53db54 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 24 Nov 2021 09:09:47 +1100 Subject: [PATCH] Tasks page refactor (#1949) * Add scan dialog * Add Auto Tag dialog * Refactor and combine Generate dialog * Add clean dialog * Support scan task default setting * Support saving auto tag defaults * Support for generate defaults --- graphql/documents/data/config.graphql | 34 + graphql/schema/types/config.graphql | 6 + graphql/schema/types/metadata.graphql | 51 ++ pkg/api/resolver_mutation_configure.go | 12 + pkg/api/resolver_query_configuration.go | 3 + pkg/manager/config/config.go | 61 ++ .../components/Changelog/versions/v0120.md | 1 + .../src/components/Dialogs/AutoTagDialog.tsx | 262 +++++++ .../src/components/Dialogs/CleanDialog.tsx | 198 +++++ .../src/components/Dialogs/GenerateDialog.tsx | 557 +++++++++++++ .../Dialogs/IdentifyDialog/IdentifyDialog.tsx | 22 +- .../Dialogs/IdentifyDialog/styles.scss | 7 - .../components/Dialogs/ScanDialog/Options.tsx | 108 +++ .../Dialogs/ScanDialog/ScanDialog.tsx | 209 +++++ ui/v2.5/src/components/Dialogs/styles.scss | 8 + ui/v2.5/src/components/Help/styles.scss | 2 +- .../components/Scenes/SceneDetails/Scene.tsx | 4 +- .../components/Scenes/SceneGenerateDialog.tsx | 322 -------- ui/v2.5/src/components/Scenes/SceneList.tsx | 4 +- .../SettingsTasksPanel/GenerateButton.tsx | 128 --- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 729 ++++++++---------- ui/v2.5/src/components/Settings/styles.scss | 26 + .../src/components/Shared/OperationButton.tsx | 2 +- ui/v2.5/src/index.scss | 9 +- ui/v2.5/src/locales/de-DE.json | 2 - ui/v2.5/src/locales/en-GB.json | 15 +- ui/v2.5/src/locales/es-ES.json | 2 - ui/v2.5/src/locales/it-IT.json | 2 - ui/v2.5/src/locales/pt-BR.json | 2 - ui/v2.5/src/locales/sv-SE.json | 2 - ui/v2.5/src/locales/zh-CN.json | 2 - ui/v2.5/src/locales/zh-TW.json | 2 - ui/v2.5/src/styles/_theme.scss | 3 +- 33 files changed, 1903 insertions(+), 894 deletions(-) create mode 100644 ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx create mode 100644 ui/v2.5/src/components/Dialogs/CleanDialog.tsx create mode 100644 ui/v2.5/src/components/Dialogs/GenerateDialog.tsx create mode 100644 ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx create mode 100644 ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx create mode 100644 ui/v2.5/src/components/Dialogs/styles.scss delete mode 100644 ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx delete mode 100644 ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 4964ab3cd..41931296d 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -106,6 +106,16 @@ fragment ScraperSourceData on ScraperSource { } fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult { + scan { + useFileMetadata + stripFileExtension + scanGeneratePreviews + scanGenerateImagePreviews + scanGenerateSprites + scanGeneratePhashes + scanGenerateThumbnails + } + identify { sources { source { @@ -120,6 +130,30 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult { } } + autoTag { + performers + studios + tags + } + + generate { + sprites + previews + imagePreviews + previewOptions { + previewSegments + previewSegmentDuration + previewExcludeStart + previewExcludeEnd + previewPreset + } + markers + markerImagePreviews + markerScreenshots + transcodes + phashes + } + deleteFile deleteGenerated } diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 6e8699a10..42259de12 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -333,7 +333,10 @@ type ConfigScrapingResult { } type ConfigDefaultSettingsResult { + scan: ScanMetadataOptions identify: IdentifyMetadataTaskOptions + autoTag: AutoTagMetadataOptions + generate: GenerateMetadataOptions """If true, delete file checkbox will be checked by default""" deleteFile: Boolean @@ -342,7 +345,10 @@ type ConfigDefaultSettingsResult { } input ConfigDefaultSettingsInput { + scan: ScanMetadataInput identify: IdentifyMetadataInput + autoTag: AutoTagMetadataInput + generate: GenerateMetadataInput """If true, delete file checkbox will be checked by default""" deleteFile: Boolean diff --git a/graphql/schema/types/metadata.graphql b/graphql/schema/types/metadata.graphql index bb2f5643f..523be8c5e 100644 --- a/graphql/schema/types/metadata.graphql +++ b/graphql/schema/types/metadata.graphql @@ -33,6 +33,31 @@ input GeneratePreviewOptionsInput { previewPreset: PreviewPreset } +type GenerateMetadataOptions { + sprites: Boolean + previews: Boolean + imagePreviews: Boolean + previewOptions: GeneratePreviewOptions + markers: Boolean + markerImagePreviews: Boolean + markerScreenshots: Boolean + transcodes: Boolean + phashes: Boolean +} + +type GeneratePreviewOptions { + """Number of segments in a preview file""" + previewSegments: Int + """Preview segment duration, in seconds""" + previewSegmentDuration: Float + """Duration of start of video to exclude when generating previews""" + previewExcludeStart: String + """Duration of end of video to exclude when generating previews""" + previewExcludeEnd: String + """Preset when generating preview""" + previewPreset: PreviewPreset +} + input ScanMetadataInput { paths: [String!] """Set name, date, details from metadata (if present)""" @@ -51,6 +76,23 @@ input ScanMetadataInput { scanGenerateThumbnails: Boolean } +type ScanMetadataOptions { + """Set name, date, details from metadata (if present)""" + useFileMetadata: Boolean! + """Strip file extension from title""" + stripFileExtension: Boolean! + """Generate previews during scan""" + scanGeneratePreviews: Boolean! + """Generate image previews during scan""" + scanGenerateImagePreviews: Boolean! + """Generate sprites during scan""" + scanGenerateSprites: Boolean! + """Generate phashes during scan""" + scanGeneratePhashes: Boolean! + """Generate image thumbnails during scan""" + scanGenerateThumbnails: Boolean! +} + input CleanMetadataInput { """Do a dry run. Don't delete any files""" dryRun: Boolean! @@ -67,6 +109,15 @@ input AutoTagMetadataInput { tags: [String!] } +type AutoTagMetadataOptions { + """IDs of performers to tag files with, or "*" for all""" + performers: [String!] + """IDs of studios to tag files with, or "*" for all""" + studios: [String!] + """IDs of tags to tag files with, or "*" for all""" + tags: [String!] +} + enum IdentifyFieldStrategy { """Never sets the field value""" IGNORE diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index 8273cc7e9..f7af4df5f 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -394,6 +394,18 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C c.Set(config.DefaultIdentifySettings, input.Identify) } + if input.Scan != nil { + c.Set(config.DefaultScanSettings, input.Scan) + } + + if input.AutoTag != nil { + c.Set(config.DefaultAutoTagSettings, input.AutoTag) + } + + if input.Generate != nil { + c.Set(config.DefaultGenerateSettings, input.Generate) + } + if input.DeleteFile != nil { c.Set(config.DeleteFileDefault, *input.DeleteFile) } diff --git a/pkg/api/resolver_query_configuration.go b/pkg/api/resolver_query_configuration.go index 25a43275b..bfb1ab808 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -174,6 +174,9 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult { return &models.ConfigDefaultSettingsResult{ Identify: config.GetDefaultIdentifySettings(), + Scan: config.GetDefaultScanSettings(), + AutoTag: config.GetDefaultAutoTagSettings(), + Generate: config.GetDefaultGenerateSettings(), DeleteFile: &deleteFileDefault, DeleteGenerated: &deleteGeneratedDefault, } diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 1482e4982..bfe1b7003 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -168,7 +168,11 @@ const ( LogAccess = "logAccess" defaultLogAccess = true + // Default settings + DefaultScanSettings = "defaults.scan_task" DefaultIdentifySettings = "defaults.identify_task" + DefaultAutoTagSettings = "defaults.auto_tag_task" + DefaultGenerateSettings = "defaults.generate_task" DeleteFileDefault = "defaults.delete_file" DeleteGeneratedDefault = "defaults.delete_generated" @@ -953,6 +957,63 @@ func (i *Instance) GetDefaultIdentifySettings() *models.IdentifyMetadataTaskOpti return nil } +// GetDefaultScanSettings returns the default Scan task settings. +// Returns nil if the settings could not be unmarshalled, or if it +// has not been set. +func (i *Instance) GetDefaultScanSettings() *models.ScanMetadataOptions { + i.RLock() + defer i.RUnlock() + v := i.viper(DefaultScanSettings) + + if v.IsSet(DefaultScanSettings) { + var ret models.ScanMetadataOptions + if err := v.UnmarshalKey(DefaultScanSettings, &ret); err != nil { + return nil + } + return &ret + } + + return nil +} + +// GetDefaultAutoTagSettings returns the default Scan task settings. +// Returns nil if the settings could not be unmarshalled, or if it +// has not been set. +func (i *Instance) GetDefaultAutoTagSettings() *models.AutoTagMetadataOptions { + i.RLock() + defer i.RUnlock() + v := i.viper(DefaultAutoTagSettings) + + if v.IsSet(DefaultAutoTagSettings) { + var ret models.AutoTagMetadataOptions + if err := v.UnmarshalKey(DefaultAutoTagSettings, &ret); err != nil { + return nil + } + return &ret + } + + return nil +} + +// GetDefaultGenerateSettings returns the default Scan task settings. +// Returns nil if the settings could not be unmarshalled, or if it +// has not been set. +func (i *Instance) GetDefaultGenerateSettings() *models.GenerateMetadataOptions { + i.RLock() + defer i.RUnlock() + v := i.viper(DefaultGenerateSettings) + + if v.IsSet(DefaultGenerateSettings) { + var ret models.GenerateMetadataOptions + if err := v.UnmarshalKey(DefaultGenerateSettings, &ret); err != nil { + return nil + } + return &ret + } + + return nil +} + // GetTrustedProxies returns a comma separated list of ip addresses that should allow proxying. // When empty, allow from any private network func (i *Instance) GetTrustedProxies() []string { diff --git a/ui/v2.5/src/components/Changelog/versions/v0120.md b/ui/v2.5/src/components/Changelog/versions/v0120.md index d4d67339c..50c5dd9fe 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0120.md +++ b/ui/v2.5/src/components/Changelog/versions/v0120.md @@ -1,4 +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)) * 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)) diff --git a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx new file mode 100644 index 000000000..960467463 --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx @@ -0,0 +1,262 @@ +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 = ({ + options, + setOptions: setOptionsState, +}) => { + const intl = useIntl(); + + const { performers, studios, tags } = options; + const wildcard = ["*"]; + + function toggle(v?: GQL.Maybe) { + if (!v?.length) { + return wildcard; + } + return []; + } + + function setOptions(input: Partial) { + setOptionsState({ ...options, ...input }); + } + + return ( + + setOptions({ performers: toggle(performers) })} + /> + setOptions({ studios: toggle(studios) })} + /> + setOptions({ tags: toggle(tags) })} + /> + + ); +}; + +interface IAutoTagDialogProps { + onClose: () => void; +} + +export const AutoTagDialog: React.FC = ({ onClose }) => { + const [configureDefaults] = useConfigureDefaults(); + + const [options, setOptions] = useState({ + performers: ["*"], + studios: ["*"], + tags: ["*"], + }); + const [paths, setPaths] = useState([]); + 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 ? ( +
+ : +
    + {paths.map((p) => ( +
  • {p}
  • + ))} +
+
+ ) : ( + + . + + ); + + function onClick() { + setAnimation(false); + setSettingPaths(true); + } + + return ( + +
+ {message} +
+ +
+
+
+ ); + }, [intl, paths]); + + if (configError) return
{configError}
; + if (!configData) return
; + + 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 ( + { + if (p) { + setPaths(p); + } + setSettingPaths(false); + }} + /> + ); + } + + if (showManual) { + return ( + setShowManual(false)} + defaultActiveTab="AutoTagging.md" + /> + ); + } + + return ( + onClose(), + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "secondary", + }} + disabled={savingDefaults} + footerButtons={ + + + + } + leftFooterButtons={ + + } + > +
+ {selectionStatus} + setOptions(o)} /> + +
+ ); +}; + +export default AutoTagDialog; diff --git a/ui/v2.5/src/components/Dialogs/CleanDialog.tsx b/ui/v2.5/src/components/Dialogs/CleanDialog.tsx new file mode 100644 index 000000000..8a240093c --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/CleanDialog.tsx @@ -0,0 +1,198 @@ +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 = ({ + options, + setOptions: setOptionsState, +}) => { + const intl = useIntl(); + + function setOptions(input: Partial) { + setOptionsState({ ...options, ...input }); + } + + return ( + + setOptions({ dryRun: !options.dryRun })} + /> + + ); +}; + +interface ICleanDialog { + onClose: () => void; +} + +export const CleanDialog: React.FC = ({ onClose }) => { + const [options, setOptions] = useState({ + dryRun: false, + }); + // TODO - selective clean + // const [paths, setPaths] = useState([]); + // 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 ( +

{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}

+ ); + } else { + return ( +

+ {intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })} +

+ ); + } + }, [options.dryRun, intl]); + + // const selectionStatus = useMemo(() => { + // const message = paths.length ? ( + //
+ // : + //
    + // {paths.map((p) => ( + //
  • {p}
  • + // ))} + //
+ //
+ // ) : ( + // + // . + // + // ); + + // function onClick() { + // setAnimation(false); + // setSettingPaths(true); + // } + + // return ( + // + //
+ // {message} + //
+ // + //
+ //
+ //
+ // ); + // }, [intl, paths]); + + if (configError) return
{configError}
; + if (!configData) return
; + + 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 ( + // { + // if (p) { + // setPaths(p); + // } + // setSettingPaths(false); + // }} + // /> + // ); + // } + + if (showManual) { + return ( + setShowManual(false)} + defaultActiveTab="Tasks.md" + /> + ); + } + + return ( + onClose(), + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "secondary", + }} + leftFooterButtons={ + + } + > +
+ setOptions(o)} /> + {message} + +
+ ); +}; + +export default CleanDialog; diff --git a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx new file mode 100644 index 000000000..8674cff54 --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -0,0 +1,557 @@ +import React, { useState, useEffect, useMemo } from "react"; +import { Form, Button, Collapse } from "react-bootstrap"; +import { + mutateMetadataGenerate, + useConfigureDefaults, +} from "src/core/StashService"; +import { Modal, Icon, OperationButton } from "src/components/Shared"; +import { useToast } from "src/hooks"; +import * as GQL from "src/core/generated-graphql"; +import { FormattedMessage, useIntl } from "react-intl"; +import { ConfigurationContext } from "src/hooks/Config"; +// import { DirectorySelectionDialog } from "../Settings/SettingsTasksPanel/DirectorySelectionDialog"; +import { Manual } from "../Help/Manual"; +import { withoutTypename } from "src/utils"; + +interface IGenerateOptions { + options: GQL.GenerateMetadataInput; + setOptions: (s: GQL.GenerateMetadataInput) => void; +} + +const GenerateOptions: React.FC = ({ + options, + setOptions: setOptionsState, +}) => { + const intl = useIntl(); + + const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false); + + const previewOptions: GQL.GeneratePreviewOptionsInput = + options.previewOptions ?? {}; + + function setOptions(input: Partial) { + setOptionsState({ ...options, ...input }); + } + + function setPreviewOptions(input: Partial) { + setOptions({ + previewOptions: { + ...previewOptions, + ...input, + }, + }); + } + + return ( + + + setOptions({ previews: !options.previews })} + /> +
+
+ + setOptions({ imagePreviews: !options.imagePreviews }) + } + className="ml-2 flex-grow" + /> +
+
+ + + + + + + +
+ {intl.formatMessage({ + id: "dialogs.scene_gen.preview_preset_head", + })} +
+ + setPreviewOptions({ + previewPreset: e.currentTarget.value as GQL.PreviewPreset, + }) + } + > + {Object.keys(GQL.PreviewPreset).map((p) => ( + + ))} + + + {intl.formatMessage({ + id: "dialogs.scene_gen.preview_preset_desc", + })} + +
+ + +
+ {intl.formatMessage({ + id: "dialogs.scene_gen.preview_seg_count_head", + })} +
+ + setPreviewOptions({ + previewSegments: Number.parseInt( + e.currentTarget.value, + 10 + ), + }) + } + /> + + {intl.formatMessage({ + id: "dialogs.scene_gen.preview_seg_count_desc", + })} + +
+ + +
+ {intl.formatMessage({ + id: "dialogs.scene_gen.preview_seg_duration_head", + })} +
+ + setPreviewOptions({ + previewSegmentDuration: Number.parseFloat( + e.currentTarget.value + ), + }) + } + /> + + {intl.formatMessage({ + id: "dialogs.scene_gen.preview_seg_duration_desc", + })} + +
+ + +
+ {intl.formatMessage({ + id: "dialogs.scene_gen.preview_exclude_start_time_head", + })} +
+ + setPreviewOptions({ + previewExcludeStart: e.currentTarget.value, + }) + } + /> + + {intl.formatMessage({ + id: "dialogs.scene_gen.preview_exclude_start_time_desc", + })} + +
+ + +
+ {intl.formatMessage({ + id: "dialogs.scene_gen.preview_exclude_end_time_head", + })} +
+ + setPreviewOptions({ + previewExcludeEnd: e.currentTarget.value, + }) + } + /> + + {intl.formatMessage({ + id: "dialogs.scene_gen.preview_exclude_end_time_desc", + })} + +
+
+
+
+
+ + + setOptions({ sprites: !options.sprites })} + /> + + setOptions({ markers: !options.markers })} + /> +
+
+ + + setOptions({ + markerImagePreviews: !options.markerImagePreviews, + }) + } + className="ml-2 flex-grow" + /> + + setOptions({ markerScreenshots: !options.markerScreenshots }) + } + className="ml-2 flex-grow" + /> + +
+
+ + + setOptions({ transcodes: !options.transcodes })} + /> + setOptions({ phashes: !options.phashes })} + /> + + +
+ + setOptions({ overwrite: !options.overwrite })} + /> + +
+
+ ); +}; + +interface ISceneGenerateDialog { + selectedIds?: string[]; + onClose: () => void; +} + +export const GenerateDialog: React.FC = ({ + selectedIds, + onClose, +}) => { + const { configuration } = React.useContext(ConfigurationContext); + const [configureDefaults] = useConfigureDefaults(); + + function getDefaultOptions(): GQL.GenerateMetadataInput { + return { + sprites: true, + phashes: true, + previews: true, + markers: true, + previewOptions: { + previewSegments: 0, + previewSegmentDuration: 0, + previewPreset: GQL.PreviewPreset.Slow, + }, + }; + } + + const [options, setOptions] = useState( + getDefaultOptions() + ); + const [configRead, setConfigRead] = useState(false); + const [paths /* , setPaths */] = useState([]); + const [showManual, setShowManual] = useState(false); + // const [settingPaths, setSettingPaths] = useState(false); + const [savingDefaults, setSavingDefaults] = useState(false); + const [animation, setAnimation] = useState(true); + + const intl = useIntl(); + const Toast = useToast(); + + useEffect(() => { + if (configRead) { + return; + } + + if (configuration?.defaults.generate) { + const { generate } = configuration.defaults; + setOptions(withoutTypename(generate)); + setConfigRead(true); + } else if (configuration?.general) { + // backwards compatibility + const { general } = configuration; + setOptions((existing) => ({ + ...existing, + previewOptions: { + ...existing.previewOptions, + previewSegments: + general.previewSegments ?? existing.previewOptions?.previewSegments, + previewSegmentDuration: + general.previewSegmentDuration ?? + existing.previewOptions?.previewSegmentDuration, + previewExcludeStart: + general.previewExcludeStart ?? + existing.previewOptions?.previewExcludeStart, + previewExcludeEnd: + general.previewExcludeEnd ?? + existing.previewOptions?.previewExcludeEnd, + previewPreset: + general.previewPreset ?? existing.previewOptions?.previewPreset, + }, + })); + setConfigRead(true); + } + }, [configuration, configRead]); + + const selectionStatus = useMemo(() => { + if (selectedIds) { + return ( + + + . + + ); + } + const message = paths.length ? ( +
+ : +
    + {paths.map((p) => ( +
  • {p}
  • + ))} +
+
+ ) : ( + + + . + + ); + + // function onClick() { + // setAnimation(false); + // setSettingPaths(true); + // } + + return ( + +
+ {message} + {/*
+ +
*/} +
+
+ ); + }, [selectedIds, intl, paths]); + + async function onGenerate() { + try { + await mutateMetadataGenerate(options); + Toast.success({ + content: intl.formatMessage( + { id: "config.tasks.added_job_to_queue" }, + { operation_name: intl.formatMessage({ id: "actions.generate" }) } + ), + }); + } catch (e) { + Toast.error(e); + } finally { + onClose(); + } + } + + function makeDefaultGenerateInput() { + const ret = options; + // const { paths: _paths, ...withoutSpecifics } = ret; + const { overwrite: _overwrite, ...withoutSpecifics } = ret; + return withoutSpecifics; + } + + function onShowManual() { + setAnimation(false); + setShowManual(true); + } + + async function setAsDefault() { + try { + setSavingDefaults(true); + await configureDefaults({ + variables: { + input: { + generate: makeDefaultGenerateInput(), + }, + }, + }); + + Toast.success({ + content: intl.formatMessage( + { id: "config.tasks.defaults_set" }, + { action: intl.formatMessage({ id: "actions.generate" }) } + ), + }); + } catch (e) { + Toast.error(e); + } finally { + setSavingDefaults(false); + } + } + + // if (settingPaths) { + // return ( + // { + // if (p) { + // setPaths(p); + // } + // setSettingPaths(false); + // }} + // /> + // ); + // } + + if (showManual) { + return ( + setShowManual(false)} + defaultActiveTab="Tasks.md" + /> + ); + } + + return ( + onClose(), + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "secondary", + }} + disabled={savingDefaults} + footerButtons={ + + + + } + leftFooterButtons={ + + } + > +
+ {selectionStatus} + + +
+ ); +}; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index f4e91b4ea..180169aec 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -1,12 +1,12 @@ import React, { useState, useEffect, useMemo } from "react"; -import { Button, Form, Spinner } from "react-bootstrap"; +import { Button, Form } from "react-bootstrap"; import { mutateMetadataIdentify, useConfiguration, useConfigureDefaults, useListSceneScrapers, } from "src/core/StashService"; -import { Icon, Modal } from "src/components/Shared"; +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"; @@ -159,7 +159,7 @@ export const IdentifyDialog: React.FC = ({ } return ( - +
{message}
@@ -346,6 +346,13 @@ export const IdentifyDialog: React.FC = ({ }, }, }); + + Toast.success({ + content: intl.formatMessage( + { id: "config.tasks.defaults_set" }, + { action: intl.formatMessage({ id: "actions.identify" }) } + ), + }); } catch (e) { Toast.error(e); } finally { @@ -409,16 +416,13 @@ export const IdentifyDialog: React.FC = ({ }} disabled={editingField || savingDefaults || sources.length === 0} footerButtons={ - + } leftFooterButtons={ +
+
+
+ ); + }, [intl, paths]); + + if (!configuration) return
; + + 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 ( + { + if (p) { + setPaths(p); + } + setSettingPaths(false); + }} + /> + ); + } + + if (showManual) { + return ( + setShowManual(false)} + defaultActiveTab="Tasks.md" + /> + ); + } + + return ( + onClose(), + text: intl.formatMessage({ id: "actions.cancel" }), + variant: "secondary", + }} + disabled={savingDefaults} + footerButtons={ + + + + } + leftFooterButtons={ + + } + > +
+ {selectionStatus} + setOptions(o)} /> + +
+ ); +}; + +export default ScanDialog; diff --git a/ui/v2.5/src/components/Dialogs/styles.scss b/ui/v2.5/src/components/Dialogs/styles.scss new file mode 100644 index 000000000..36c350c2d --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/styles.scss @@ -0,0 +1,8 @@ +@import "IdentifyDialog/styles.scss"; + +.dialog-selected-folders { + & > div { + display: flex; + justify-content: space-between; + } +} diff --git a/ui/v2.5/src/components/Help/styles.scss b/ui/v2.5/src/components/Help/styles.scss index 93f6f3975..311876ea2 100644 --- a/ui/v2.5/src/components/Help/styles.scss +++ b/ui/v2.5/src/components/Help/styles.scss @@ -12,7 +12,7 @@ &-header, &-body { - background-color: #30404d; + background-color: $card-bg; color: $text-color; overflow-y: hidden; } diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index 8cef46024..be1be4d5f 100644 --- a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx +++ b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx @@ -35,7 +35,7 @@ import { ExternalPlayerButton } from "./ExternalPlayerButton"; import { SceneMoviePanel } from "./SceneMoviePanel"; import { SceneGalleriesPanel } from "./SceneGalleriesPanel"; import { DeleteScenesDialog } from "../DeleteScenesDialog"; -import { SceneGenerateDialog } from "../SceneGenerateDialog"; +import { GenerateDialog } from "../../Dialogs/GenerateDialog"; import { SceneVideoFilterPanel } from "./SceneVideoFilterPanel"; import { OrganizedButton } from "./OrganizedButton"; @@ -332,7 +332,7 @@ const ScenePage: React.FC = ({ scene, refetch }) => { function maybeRenderSceneGenerateDialog() { if (isGenerateDialogOpen) { return ( - { setIsGenerateDialogOpen(false); diff --git a/ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx b/ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx deleted file mode 100644 index e57070f2f..000000000 --- a/ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx +++ /dev/null @@ -1,322 +0,0 @@ -import React, { useState, useEffect } from "react"; -import { Form, Button, Collapse } from "react-bootstrap"; -import { mutateMetadataGenerate } from "src/core/StashService"; -import { Modal, Icon } from "src/components/Shared"; -import { useToast } from "src/hooks"; -import * as GQL from "src/core/generated-graphql"; -import { useIntl } from "react-intl"; -import { ConfigurationContext } from "src/hooks/Config"; - -interface ISceneGenerateDialogProps { - selectedIds: string[]; - onClose: () => void; -} - -export const SceneGenerateDialog: React.FC = ( - props: ISceneGenerateDialogProps -) => { - const { configuration } = React.useContext(ConfigurationContext); - - const [sprites, setSprites] = useState(true); - const [phashes, setPhashes] = useState(true); - const [previews, setPreviews] = useState(true); - const [markers, setMarkers] = useState(true); - const [transcodes, setTranscodes] = useState(false); - const [overwrite, setOverwrite] = useState(true); - const [imagePreviews, setImagePreviews] = useState(false); - - const [previewSegments, setPreviewSegments] = useState(0); - const [previewSegmentDuration, setPreviewSegmentDuration] = useState( - 0 - ); - const [previewExcludeStart, setPreviewExcludeStart] = useState< - string | undefined - >(undefined); - const [previewExcludeEnd, setPreviewExcludeEnd] = useState< - string | undefined - >(undefined); - const [previewPreset, setPreviewPreset] = useState( - GQL.PreviewPreset.Slow - ); - const [markerImagePreviews, setMarkerImagePreviews] = useState(false); - const [markerScreenshots, setMarkerScreenshots] = useState(false); - - const [previewOptionsOpen, setPreviewOptionsOpen] = useState(false); - - const intl = useIntl(); - const Toast = useToast(); - - useEffect(() => { - if (!configuration) return; - - if (configuration.general) { - setPreviewSegments(configuration.general.previewSegments); - setPreviewSegmentDuration(configuration.general.previewSegmentDuration); - setPreviewExcludeStart(configuration.general.previewExcludeStart); - setPreviewExcludeEnd(configuration.general.previewExcludeEnd); - setPreviewPreset(configuration.general.previewPreset); - } - }, [configuration]); - - async function onGenerate() { - try { - await mutateMetadataGenerate({ - sprites, - phashes, - previews, - imagePreviews: previews && imagePreviews, - markers, - markerImagePreviews: markers && markerImagePreviews, - markerScreenshots: markers && markerScreenshots, - transcodes, - overwrite, - sceneIDs: props.selectedIds, - previewOptions: { - previewPreset: (previewPreset as GQL.PreviewPreset) ?? undefined, - previewSegments, - previewSegmentDuration, - previewExcludeStart, - previewExcludeEnd, - }, - }); - Toast.success({ - content: intl.formatMessage({ id: "toast.started_generating" }), - }); - } catch (e) { - Toast.error(e); - } finally { - props.onClose(); - } - } - - return ( - props.onClose(), - text: intl.formatMessage({ id: "actions.cancel" }), - variant: "secondary", - }} - > -
- - setPreviews(!previews)} - /> -
-
- setImagePreviews(!imagePreviews)} - className="ml-2 flex-grow" - /> -
-
- - -
- -
- {intl.formatMessage({ - id: "dialogs.scene_gen.preview_preset_head", - })} -
- ) => - setPreviewPreset(e.currentTarget.value) - } - > - {Object.keys(GQL.PreviewPreset).map((p) => ( - - ))} - - - {intl.formatMessage({ - id: "dialogs.scene_gen.preview_preset_desc", - })} - -
- - -
- {intl.formatMessage({ - id: "dialogs.scene_gen.preview_seg_count_head", - })} -
- ) => - setPreviewSegments( - Number.parseInt(e.currentTarget.value, 10) - ) - } - /> - - {intl.formatMessage({ - id: "dialogs.scene_gen.preview_seg_count_desc", - })} - -
- - -
- {intl.formatMessage({ - id: "dialogs.scene_gen.preview_seg_duration_head", - })} -
- ) => - setPreviewSegmentDuration( - Number.parseFloat(e.currentTarget.value) - ) - } - /> - - {intl.formatMessage({ - id: "dialogs.scene_gen.preview_seg_duration_desc", - })} - -
- - -
- {intl.formatMessage({ - id: "dialogs.scene_gen.preview_exclude_start_time_head", - })} -
- ) => - setPreviewExcludeStart(e.currentTarget.value) - } - /> - - {intl.formatMessage({ - id: "dialogs.scene_gen.preview_exclude_start_time_desc", - })} - -
- - -
- {intl.formatMessage({ - id: "dialogs.scene_gen.preview_exclude_end_time_head", - })} -
- ) => - setPreviewExcludeEnd(e.currentTarget.value) - } - /> - - {intl.formatMessage({ - id: "dialogs.scene_gen.preview_exclude_end_time_desc", - })} - -
-
-
-
- setSprites(!sprites)} - /> - setMarkers(!markers)} - /> -
-
- - setMarkerImagePreviews(!markerImagePreviews)} - className="ml-2 flex-grow" - /> - setMarkerScreenshots(!markerScreenshots)} - className="ml-2 flex-grow" - /> - -
- setTranscodes(!transcodes)} - /> - setPhashes(!phashes)} - /> - -
- setOverwrite(!overwrite)} - /> -
-
-
- ); -}; diff --git a/ui/v2.5/src/components/Scenes/SceneList.tsx b/ui/v2.5/src/components/Scenes/SceneList.tsx index d5d38d6de..143d552a0 100644 --- a/ui/v2.5/src/components/Scenes/SceneList.tsx +++ b/ui/v2.5/src/components/Scenes/SceneList.tsx @@ -19,7 +19,7 @@ import { WallPanel } from "../Wall/WallPanel"; import { SceneListTable } from "./SceneListTable"; import { EditScenesDialog } from "./EditScenesDialog"; import { DeleteScenesDialog } from "./DeleteScenesDialog"; -import { SceneGenerateDialog } from "./SceneGenerateDialog"; +import { GenerateDialog } from "../Dialogs/GenerateDialog"; import { ExportDialog } from "../Shared/ExportDialog"; import { SceneCardsGrid } from "./SceneCardsGrid"; import { TaggerContext } from "../Tagger/context"; @@ -180,7 +180,7 @@ export const SceneList: React.FC = ({ if (isGenerateDialogOpen) { return ( <> - { setIsGenerateDialogOpen(false); diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx deleted file mode 100644 index b251d081d..000000000 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/GenerateButton.tsx +++ /dev/null @@ -1,128 +0,0 @@ -import React, { useState } from "react"; -import { Button, Form } from "react-bootstrap"; -import { FormattedMessage, useIntl } from "react-intl"; -import { mutateMetadataGenerate } from "src/core/StashService"; -import { useToast } from "src/hooks"; - -export const GenerateButton: React.FC = () => { - const Toast = useToast(); - const intl = useIntl(); - const [sprites, setSprites] = useState(true); - const [phashes, setPhashes] = useState(true); - const [previews, setPreviews] = useState(true); - const [markers, setMarkers] = useState(true); - const [transcodes, setTranscodes] = useState(false); - const [imagePreviews, setImagePreviews] = useState(false); - const [markerImagePreviews, setMarkerImagePreviews] = useState(false); - const [markerScreenshots, setMarkerScreenshots] = useState(false); - - async function onGenerate() { - try { - await mutateMetadataGenerate({ - sprites, - phashes, - previews, - imagePreviews: previews && imagePreviews, - markers, - markerImagePreviews: markers && markerImagePreviews, - markerScreenshots: markers && markerScreenshots, - transcodes, - }); - Toast.success({ - content: intl.formatMessage({ - id: "toast.added_generation_job_to_queue", - }), - }); - } catch (e) { - Toast.error(e); - } - } - - return ( - <> - - setPreviews(!previews)} - /> -
-
- setImagePreviews(!imagePreviews)} - className="ml-2 flex-grow" - /> -
- setSprites(!sprites)} - /> - setMarkers(!markers)} - /> -
-
- - setMarkerImagePreviews(!markerImagePreviews)} - className="ml-2 flex-grow" - /> - setMarkerScreenshots(!markerScreenshots)} - className="ml-2 flex-grow" - /> - -
- setTranscodes(!transcodes)} - /> - setPhashes(!phashes)} - /> -
- - - - {intl.formatMessage({ id: "config.tasks.generate_desc" })} - - - - ); -}; diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index f7f53f387..6b15e34f8 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -1,69 +1,69 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, Form } from "react-bootstrap"; +import { Button, ButtonGroup, Card, Form } from "react-bootstrap"; import { mutateMetadataImport, - mutateMetadataClean, - mutateMetadataScan, - mutateMetadataAutoTag, 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 } from "src/utils"; +import { downloadFile, withoutTypename } from "src/utils"; import IdentifyDialog from "src/components/Dialogs/IdentifyDialog/IdentifyDialog"; -import { GenerateButton } from "./GenerateButton"; import { ImportDialog } from "./ImportDialog"; -import { DirectorySelectionDialog } from "./DirectorySelectionDialog"; 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; type PluginTask = Pick; +interface ITask { + description?: React.ReactNode; +} + +const Task: React.FC> = ({ + children, + description, +}) => ( +
+ {children} + {description ? ( + {description} + ) : undefined} +
+); + export const SettingsTasksPanel: React.FC = () => { const intl = useIntl(); const Toast = useToast(); const [dialogOpen, setDialogOpenState] = useState({ importAlert: false, - cleanAlert: false, import: false, clean: false, scan: false, autoTag: false, identify: false, + generate: false, }); type DialogOpenState = typeof dialogOpen; const [isBackupRunning, setIsBackupRunning] = useState(false); - const [useFileMetadata, setUseFileMetadata] = useState(false); - const [stripFileExtension, setStripFileExtension] = useState(false); - const [scanGeneratePreviews, setScanGeneratePreviews] = useState( - false - ); - const [scanGenerateSprites, setScanGenerateSprites] = useState( - false - ); - const [scanGeneratePhashes, setScanGeneratePhashes] = useState( - false - ); - const [scanGenerateThumbnails, setScanGenerateThumbnails] = useState( - false - ); - const [cleanDryRun, setCleanDryRun] = useState(false); - const [ - scanGenerateImagePreviews, - setScanGenerateImagePreviews, - ] = useState(false); - const [autoTagPerformers, setAutoTagPerformers] = useState(true); - const [autoTagStudios, setAutoTagStudios] = useState(true); - const [autoTagTags, setAutoTagTags] = useState(true); + const { configuration } = React.useContext(ConfigurationContext); const plugins = usePlugins(); @@ -105,41 +105,12 @@ export const SettingsTasksPanel: React.FC = () => { ); } - function onClean() { - setDialogOpen({ cleanAlert: false }); - mutateMetadataClean({ - dryRun: cleanDryRun, - }); - } - - function renderCleanAlert() { - let msg; - if (cleanDryRun) { - msg = ( -

{intl.formatMessage({ id: "actions.tasks.dry_mode_selected" })}

- ); - } else { - msg = ( -

- {intl.formatMessage({ id: "actions.tasks.clean_confirm_message" })} -

- ); + function renderCleanDialog() { + if (!dialogOpen.clean) { + return; } - return ( - setDialogOpen({ cleanAlert: false }) }} - > - {msg} - - ); + return setDialogOpen({ clean: false })} />; } function renderImportDialog() { @@ -155,38 +126,7 @@ export const SettingsTasksPanel: React.FC = () => { return; } - return ; - } - - function onScanDialogClosed(paths?: string[]) { - if (paths) { - onScan(paths); - } - - setDialogOpen({ scan: false }); - } - - async function onScan(paths?: string[]) { - try { - await mutateMetadataScan({ - paths, - useFileMetadata, - stripFileExtension, - scanGeneratePreviews, - scanGenerateImagePreviews, - scanGenerateSprites, - scanGeneratePhashes, - scanGenerateThumbnails, - }); - Toast.success({ - content: intl.formatMessage( - { id: "config.tasks.added_job_to_queue" }, - { operation_name: intl.formatMessage({ id: "actions.scan" }) } - ), - }); - } catch (e) { - Toast.error(e); - } + return setDialogOpen({ scan: false })} />; } function renderAutoTagDialog() { @@ -194,7 +134,7 @@ export const SettingsTasksPanel: React.FC = () => { return; } - return ; + return setDialogOpen({ autoTag: false })} />; } function maybeRenderIdentifyDialog() { @@ -205,36 +145,12 @@ export const SettingsTasksPanel: React.FC = () => { ); } - function onAutoTagDialogClosed(paths?: string[]) { - if (paths) { - onAutoTag(paths); - } + function maybeRenderGenerateDialog() { + if (!dialogOpen.generate) return; - setDialogOpen({ autoTag: false }); - } - - function getAutoTagInput(paths?: string[]) { - const wildcard = ["*"]; - return { - paths, - performers: autoTagPerformers ? wildcard : [], - studios: autoTagStudios ? wildcard : [], - tags: autoTagTags ? wildcard : [], - }; - } - - async function onAutoTag(paths?: string[]) { - try { - await mutateMetadataAutoTag(getAutoTagInput(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); - } + return ( + setDialogOpen({ generate: false })} /> + ); } async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) { @@ -254,19 +170,15 @@ export const SettingsTasksPanel: React.FC = () => { return pluginTasks.map((o) => { return ( -
+ - {o.description ? ( - {o.description} - ) : undefined} -
+ ); }); } @@ -302,16 +214,20 @@ export const SettingsTasksPanel: React.FC = () => { return ( <>
-
{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}
- {taskPlugins.map((o) => { - return ( -
-
{o.name}
- {renderPluginTasks(o, o.tasks ?? [])} -
-
- ); - })} + + +
{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}
+ {taskPlugins.map((o) => { + return ( + +
{o.name}
+ + {renderPluginTasks(o, o.tasks ?? [])} + +
+ ); + })} +
); } @@ -348,6 +264,66 @@ export const SettingsTasksPanel: React.FC = () => { } } + 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 ( { return ( <> {renderImportAlert()} - {renderCleanAlert()} + {renderCleanDialog()} {renderImportDialog()} {renderScanDialog()} {renderAutoTagDialog()} {maybeRenderIdentifyDialog()} + {maybeRenderGenerateDialog()}

{intl.formatMessage({ id: "config.tasks.job_queue" })}

@@ -373,287 +350,237 @@ export const SettingsTasksPanel: React.FC = () => {
{intl.formatMessage({ id: "library" })}
- -
{intl.formatMessage({ id: "actions.scan" })}
- + setUseFileMetadata(!useFileMetadata)} - /> - + + + + + + + setStripFileExtension(!stripFileExtension)} - /> - + + + + + + + setScanGeneratePreviews(!scanGeneratePreviews)} - /> -
-
- - setScanGenerateImagePreviews(!scanGenerateImagePreviews) + > + + + + + + + + + + + + +
+ + +
{intl.formatMessage({ id: "config.tasks.generated_content" })}
+ + + + + + + + + +
+ +
+ + +
{intl.formatMessage({ id: "metadata" })}
+ + + + + + + + + + + + + +
+ +
+ + +
{intl.formatMessage({ id: "actions.backup" })}
+ + + [origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] + + ), } - className="ml-2 flex-grow" - /> -
- setScanGenerateSprites(!scanGenerateSprites)} - /> - setScanGeneratePhashes(!scanGeneratePhashes)} - /> - setScanGenerateThumbnails(!scanGenerateThumbnails)} - /> -
- - - - - {intl.formatMessage({ id: "config.tasks.scan_for_content_desc" })} - - - - -
- -
- - - - -
- - -
{intl.formatMessage({ id: "config.tasks.auto_tagging" })}
- - - setAutoTagPerformers(!autoTagPerformers)} - /> - setAutoTagStudios(!autoTagStudios)} - /> - setAutoTagTags(!autoTagTags)} - /> - - + + + - - {intl.formatMessage({ - id: "config.tasks.auto_tag_based_on_filenames", - })} - - -
-
- -
- -
{intl.formatMessage({ id: "config.tasks.generated_content" })}
- - -
-
{intl.formatMessage({ id: "config.tasks.maintenance" })}
- - setCleanDryRun(!cleanDryRun)} - /> - - - - - {intl.formatMessage({ id: "config.tasks.cleanup_desc" })} - - - -
- -
{intl.formatMessage({ id: "metadata" })}
- - - - {intl.formatMessage({ id: "config.tasks.export_to_json" })} - - - - - - - {intl.formatMessage({ id: "config.tasks.import_from_exported_json" })} - - - - - - - {intl.formatMessage({ id: "config.tasks.incremental_import" })} - - - -
- -
{intl.formatMessage({ id: "actions.backup" })}
- - - - {intl.formatMessage( - { id: "config.tasks.backup_database" }, - { - filename_format: ( - - [origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] - - ), - } - )} - - - - - - - {intl.formatMessage({ id: "config.tasks.backup_and_download" })} - + + {renderPlugins()}
-
{intl.formatMessage({ id: "config.tasks.migrations" })}
- - - - {intl.formatMessage({ id: "config.tasks.migrate_hash_files" })} - +
{intl.formatMessage({ id: "config.tasks.migrations" })}
+ + + + + +
); diff --git a/ui/v2.5/src/components/Settings/styles.scss b/ui/v2.5/src/components/Settings/styles.scss index 04ace918d..eb1fe2e51 100644 --- a/ui/v2.5/src/components/Settings/styles.scss +++ b/ui/v2.5/src/components/Settings/styles.scss @@ -161,3 +161,29 @@ } } } + +.card.task-group { + padding-bottom: 0.5rem; + padding-top: 0.5rem; + + .task { + padding-bottom: 0.5rem; + padding-top: 0.5rem; + + &:not(:last-child) { + border-bottom: 1px solid $dark-gray2; + } + } +} + +.ellipsis-button { + .btn:first-child { + border-right: 1px solid $card-bg; + } + + .btn:last-child { + border-left: 1px solid $card-bg; + padding-left: 0.25rem; + padding-right: 0.25rem; + } +} diff --git a/ui/v2.5/src/components/Shared/OperationButton.tsx b/ui/v2.5/src/components/Shared/OperationButton.tsx index 7cdc83119..a7c23dcf2 100644 --- a/ui/v2.5/src/components/Shared/OperationButton.tsx +++ b/ui/v2.5/src/components/Shared/OperationButton.tsx @@ -33,7 +33,7 @@ export const OperationButton: React.FC = (props) => { externalLoading !== undefined ? externalLoading : internalLoading; async function handleClick() { - if (operation) { + if (operation && !loading) { setLoading(true); await operation(); diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index bf30ef36a..2ac93c8d3 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -21,6 +21,7 @@ @import "src/components/Tagger/styles.scss"; @import "src/hooks/Lightbox/lightbox.scss"; @import "src/components/Dialogs/IdentifyDialog/styles.scss"; +@import "src/components/Dialogs/styles.scss"; /* stylelint-disable */ #root { @@ -381,7 +382,7 @@ div.dropdown-menu { } .card { - background-color: #30404d; + background-color: $card-bg; border-radius: 3px; box-shadow: 0 0 0 1px rgba(16, 22, 26, 0.4), 0 0 0 rgba(16, 22, 26, 0), 0 0 0 rgba(16, 22, 26, 0); @@ -671,7 +672,7 @@ div.dropdown-menu { } code { - background-color: darken($color: #30404d, $amount: 3); + background-color: darken($color: $card-bg, $amount: 3); color: $text-color; padding: 0.2em 0.4em; } @@ -689,7 +690,7 @@ div.dropdown-menu { padding: 0; } - background-color: darken($color: #30404d, $amount: 3); + background-color: darken($color: $card-bg, $amount: 3); border-radius: 3px; padding: 16px; } @@ -711,7 +712,7 @@ div.dropdown-menu { } tr:nth-child(2n) { - background-color: darken($color: #30404d, $amount: 2); + background-color: darken($color: $card-bg, $amount: 2); } td, diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index d9aec481e..5df6a5696 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -79,8 +79,6 @@ "select_all": "Alle auswählen", "select_folders": "Ordner auswählen", "select_none": "Nichts auswählen", - "selective_auto_tag": "Selektives Auto Tag", - "selective_scan": "Selektives Scannen", "set_as_default": "Als Voreinstellung festlegen", "set_back_image": "Rückseite…", "set_front_image": "Vorderseite…", diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index eec9c00cd..181d46e93 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -78,8 +78,6 @@ "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…", @@ -291,14 +289,23 @@ }, "tasks": { "added_job_to_queue": "Added {operation_name} to job queue", + "auto_tag": { + "auto_tagging_paths": "Auto Tagging the following paths", + "auto_tagging_all_paths": "Auto Tagging all paths" + }, "auto_tag_based_on_filenames": "Auto-tag content based on filenames.", "auto_tagging": "Auto Tagging", "backing_up_database": "Backing up database", "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.", + "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.", + "generate": { + "generating_scenes": "Generating for {num} {scene}", + "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_previews_during_scan": "Generate image previews during scan (animated WebP previews, only required if Preview Type is set to Animated Image)", @@ -334,6 +341,10 @@ "migrations": "Migrations", "only_dry_run": "Only perform a dry run. Don't remove anything", "plugin_tasks": "Plugin Tasks", + "scan": { + "scanning_paths": "Scanning the following paths", + "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)" }, diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index cbca5bca4..6eb17bd12 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -79,8 +79,6 @@ "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…", diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index 8690fd8bb..65cbb3275 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -79,8 +79,6 @@ "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…", diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index 9c732a2fb..33e7bc9d8 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -64,8 +64,6 @@ "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…", diff --git a/ui/v2.5/src/locales/sv-SE.json b/ui/v2.5/src/locales/sv-SE.json index 8f2394adc..a17390ea9 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -68,8 +68,6 @@ "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…", diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index 8edf779bf..c80bd746b 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -79,8 +79,6 @@ "select_all": "选择所有", "select_folders": "选择目录", "select_none": "清除选择", - "selective_auto_tag": "选择性自动生成标签", - "selective_scan": "选择性扫描", "set_as_default": "设置为默认", "set_back_image": "设置背面图…", "set_front_image": "设置正面图…", diff --git a/ui/v2.5/src/locales/zh-TW.json b/ui/v2.5/src/locales/zh-TW.json index f1382a5f7..e0c6d4ed3 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -79,8 +79,6 @@ "select_all": "全選", "select_folders": "選擇資料夾", "select_none": "清除選擇", - "selective_auto_tag": "選擇性套用標籤", - "selective_scan": "選擇性掃描", "set_as_default": "設為預設", "set_back_image": "設定背面圖…", "set_front_image": "設定正面圖…", diff --git a/ui/v2.5/src/styles/_theme.scss b/ui/v2.5/src/styles/_theme.scss index ec56fbfcd..03f7e8b8c 100644 --- a/ui/v2.5/src/styles/_theme.scss +++ b/ui/v2.5/src/styles/_theme.scss @@ -25,6 +25,7 @@ $navbar-dark-color: rgb(245, 248, 250); $popover-bg: $secondary; $dark-text: #182026; $textfield-bg: rgba(16, 22, 26, 0.3); +$card-bg: #30404d; @import "node_modules/bootstrap/scss/bootstrap"; @@ -186,7 +187,7 @@ hr { &-header, &-body, &-footer { - background-color: #30404d; + background-color: $card-bg; color: $text-color; }