diff --git a/ui/v2.5/src/components/Changelog/versions/v0120.md b/ui/v2.5/src/components/Changelog/versions/v0120.md index f1ce22d96..bd3d885c4 100644 --- a/ui/v2.5/src/components/Changelog/versions/v0120.md +++ b/ui/v2.5/src/components/Changelog/versions/v0120.md @@ -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)) diff --git a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx deleted file mode 100644 index 960467463..000000000 --- a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx +++ /dev/null @@ -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 = ({ - 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 deleted file mode 100644 index 8a240093c..000000000 --- a/ui/v2.5/src/components/Dialogs/CleanDialog.tsx +++ /dev/null @@ -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 = ({ - 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 index 8674cff54..faa7bf9b8 100644 --- a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -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 = ({ - 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 })} - /> - -
-
- ); -}; +import { GenerateOptions } from "../Settings/Tasks/GenerateOptions"; interface ISceneGenerateDialog { selectedIds?: string[]; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index 180169aec..49714e51c 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -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"; diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx deleted file mode 100644 index da5d927ae..000000000 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx +++ /dev/null @@ -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 = ({ onClose }) => { - const [configureDefaults] = useConfigureDefaults(); - - const [options, setOptions] = useState({}); - 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 { 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 ? ( -
- : -
    - {paths.map((p) => ( -
  • {p}
  • - ))} -
-
- ) : ( - - . - - ); - - function onClick() { - setAnimation(false); - setSettingPaths(true); - } - - return ( - -
- {message} -
- -
-
-
- ); - }, [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/MainNavbar.tsx b/ui/v2.5/src/components/MainNavbar.tsx index 330ffa4ad..ae7931e3e 100644 --- a/ui/v2.5/src/components/MainNavbar.tsx +++ b/ui/v2.5/src/components/MainNavbar.tsx @@ -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} > - + - - ); - }); - } - - 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 ( - <> -
- - -
{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}
- {taskPlugins.map((o) => { - return ( - -
{o.name}
- - {renderPluginTasks(o, o.tasks ?? [])} - -
- ); - })} -
- - ); - } - - 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 ( - - ); - } - - return ( - <> - {renderImportAlert()} - {renderCleanDialog()} - {renderImportDialog()} - {renderScanDialog()} - {renderAutoTagDialog()} - {maybeRenderIdentifyDialog()} - {maybeRenderGenerateDialog()} - -

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

- - - -
- - -
{intl.formatMessage({ id: "library" })}
- - - - - - - - - - - - - - - - - - - - - - - - - - -
- -
- - -
{intl.formatMessage({ id: "config.tasks.generated_content" })}
- - - - - - - - - -
- -
- - -
{intl.formatMessage({ id: "metadata" })}
- - - - - - - - - - - - - -
- -
- - -
{intl.formatMessage({ id: "actions.backup" })}
- - - [origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] - - ), - } - )} - > - - - - - - - -
- - {renderPlugins()} - -
- - -
{intl.formatMessage({ id: "config.tasks.migrations" })}
- - - - - - -
- - ); -}; diff --git a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx new file mode 100644 index 000000000..498dbb94f --- /dev/null +++ b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx @@ -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 = ({ + options, + setOptions: setOptionsState, +}) => { + const intl = useIntl(); + + function setOptions(input: Partial) { + setOptionsState({ ...options, ...input }); + } + + return ( + + setOptions({ dryRun: !options.dryRun })} + /> + + ); +}; + +interface IDataManagementTasks { + setIsBackupRunning: (v: boolean) => void; +} + +export const DataManagementTasks: React.FC = ({ + setIsBackupRunning, +}) => { + const intl = useIntl(); + const Toast = useToast(); + const [dialogOpen, setDialogOpenState] = useState({ + importAlert: false, + import: false, + clean: false, + }); + + const [cleanOptions, setCleanOptions] = useState({ + dryRun: false, + }); + + type DialogOpenState = typeof dialogOpen; + + function setDialogOpen(s: Partial) { + 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 ( + setDialogOpen({ importAlert: false }) }} + > +

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

+
+ ); + } + + function renderImportDialog() { + if (!dialogOpen.import) { + return; + } + + return setDialogOpen({ import: false })} />; + } + + function renderCleanDialog() { + let msg; + if (cleanOptions.dryRun) { + msg = ( +

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

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

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

+ ); + } + + return ( + setDialogOpen({ clean: false }) }} + > + {msg} + + ); + } + + 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 ( + + {renderImportAlert()} + {renderImportDialog()} + {renderCleanDialog()} + + +
+
{intl.formatMessage({ id: "config.tasks.maintenance" })}
+ + setCleanOptions(o)} + /> + + +
+
+ +
+ + +
{intl.formatMessage({ id: "metadata" })}
+
+ + + + + + + + + + + +
+
+ +
+ + +
{intl.formatMessage({ id: "actions.backup" })}
+
+ + [origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] + + ), + } + )} + > + + + + + + +
+
+ +
+ + +
{intl.formatMessage({ id: "config.tasks.migrations" })}
+ +
+ + + +
+
+
+ ); +}; diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx similarity index 100% rename from ui/v2.5/src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog.tsx rename to ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx diff --git a/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx new file mode 100644 index 000000000..5443d8355 --- /dev/null +++ b/ui/v2.5/src/components/Settings/Tasks/GenerateOptions.tsx @@ -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 = ({ + 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 })} + /> + +
+
+ ); +}; diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/ImportDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx similarity index 100% rename from ui/v2.5/src/components/Settings/SettingsTasksPanel/ImportDialog.tsx rename to ui/v2.5/src/components/Settings/Tasks/ImportDialog.tsx diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/JobTable.tsx b/ui/v2.5/src/components/Settings/Tasks/JobTable.tsx similarity index 100% rename from ui/v2.5/src/components/Settings/SettingsTasksPanel/JobTable.tsx rename to ui/v2.5/src/components/Settings/Tasks/JobTable.tsx diff --git a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx new file mode 100644 index 000000000..93a4a9ff4 --- /dev/null +++ b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx @@ -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 = ({ + 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) })} + /> + + ); +}; + +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({}); + const [ + autoTagOptions, + setAutoTagOptions, + ] = useState({ + 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(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) { + setDialogOpenState((v) => { + return { ...v, ...s }; + }); + } + + function renderScanDialog() { + if (!dialogOpen.scan) { + return; + } + + return ; + } + + 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 ; + } + + 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 ( + 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 ( + + {renderScanDialog()} + {renderAutoTagDialog()} + {maybeRenderIdentifyDialog()} + + +
{intl.formatMessage({ id: "library" })}
+ +
+ + + + + + + + + + + + + setAutoTagOptions(o)} + /> + + + + +
+
+ +
+ + +
{intl.formatMessage({ id: "config.tasks.generated_content" })}
+ +
+ + + + +
+
+
+ ); +}; diff --git a/ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx new file mode 100644 index 000000000..409ffb51c --- /dev/null +++ b/ui/v2.5/src/components/Settings/Tasks/PluginTasks.tsx @@ -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; +type PluginTask = Pick; + +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 ( + +
{intl.formatMessage({ id: "config.tasks.plugin_tasks" })}
+ {taskPlugins.map((o) => { + return ( + +
{o.name}
+
+ {renderPluginTasks(o, o.tasks ?? [])} +
+
+ ); + })} +
+ ); + } + + function renderPluginTasks(plugin: Plugin, pluginTasks: PluginTask[]) { + if (!pluginTasks) { + return; + } + + return pluginTasks.map((o) => { + return ( + + + + ); + }); + } + + 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 {renderPlugins()}; +}; diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx b/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx similarity index 100% rename from ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx rename to ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx index a5426d3a7..49be42779 100644 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/ScanOptions.tsx @@ -30,22 +30,6 @@ export const ScanOptions: React.FC = ({ return ( - setOptions({ useFileMetadata: !useFileMetadata })} - /> - setOptions({ stripFileExtension: !stripFileExtension })} - /> = ({ setOptions({ scanGenerateThumbnails: !scanGenerateThumbnails }) } /> + setOptions({ stripFileExtension: !stripFileExtension })} + /> + setOptions({ useFileMetadata: !useFileMetadata })} + /> ); }; diff --git a/ui/v2.5/src/components/Settings/Tasks/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/Tasks/SettingsTasksPanel.tsx new file mode 100644 index 000000000..65c47b76e --- /dev/null +++ b/ui/v2.5/src/components/Settings/Tasks/SettingsTasksPanel.tsx @@ -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(false); + + if (isBackupRunning) { + return ( + + ); + } + + return ( + <> +

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

+ + + +
+ + +
+ +
+ + + ); +}; diff --git a/ui/v2.5/src/components/Settings/Tasks/Task.tsx b/ui/v2.5/src/components/Settings/Tasks/Task.tsx new file mode 100644 index 000000000..2b516ae87 --- /dev/null +++ b/ui/v2.5/src/components/Settings/Tasks/Task.tsx @@ -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> = ({ + children, + headingID, + description, +}) => ( +
+ {headingID ? ( +
+ +
+ ) : undefined} + {children} + {description ? ( + {description} + ) : undefined} +
+); diff --git a/ui/v2.5/src/components/Settings/styles.scss b/ui/v2.5/src/components/Settings/styles.scss index eb1fe2e51..810ddf881 100644 --- a/ui/v2.5/src/components/Settings/styles.scss +++ b/ui/v2.5/src/components/Settings/styles.scss @@ -162,28 +162,20 @@ } } -.card.task-group { - padding-bottom: 0.5rem; +.task-group { padding-top: 0.5rem; - .task { + &:not(:last-child) { padding-bottom: 0.5rem; - padding-top: 0.5rem; + } + + .task { + &:not(:first-child) { + padding-top: 0.5rem; + } &:not(:last-child) { - border-bottom: 1px solid $dark-gray2; + padding-bottom: 1rem; } } } - -.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/SettingsButton.tsx b/ui/v2.5/src/components/SettingsButton.tsx new file mode 100644 index 000000000..a15203da5 --- /dev/null +++ b/ui/v2.5/src/components/SettingsButton.tsx @@ -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([]); + + 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 ( + + ); +}; diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 181d46e93..90b50ff97 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -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", diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index 4f9cfb2cc..0c979beb8 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -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…", diff --git a/ui/v2.5/src/locales/it-IT.json b/ui/v2.5/src/locales/it-IT.json index fe7a3150c..b7e7eeda9 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -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…", diff --git a/ui/v2.5/src/locales/pt-BR.json b/ui/v2.5/src/locales/pt-BR.json index 33e7bc9d8..9c732a2fb 100644 --- a/ui/v2.5/src/locales/pt-BR.json +++ b/ui/v2.5/src/locales/pt-BR.json @@ -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…", diff --git a/ui/v2.5/src/locales/sv-SE.json b/ui/v2.5/src/locales/sv-SE.json index 504db225c..ad67d127c 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -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…", diff --git a/ui/v2.5/src/locales/zh-CN.json b/ui/v2.5/src/locales/zh-CN.json index 4c54205fa..ee73f8dac 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -79,6 +79,8 @@ "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 79b667e6e..444e6955e 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -79,6 +79,8 @@ "select_all": "全選", "select_folders": "選擇資料夾", "select_none": "清除選擇", + "selective_auto_tag": "選擇性套用標籤", + "selective_scan": "選擇性掃描", "set_as_default": "設為預設", "set_back_image": "設定背面圖…", "set_front_image": "設定正面圖…",