From d2e1049606008a42ca013731620f9771d8e84317 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 3 Nov 2021 16:14:44 +1100 Subject: [PATCH 01/15] Add scan dialog --- .../components/Dialogs/ScanDialog/Options.tsx | 108 ++++++++++ .../Dialogs/ScanDialog/ScanDialog.tsx | 194 ++++++++++++++++++ .../components/Dialogs/ScanDialog/styles.scss | 6 + .../SettingsTasksPanel/SettingsTasksPanel.tsx | 133 +----------- ui/v2.5/src/index.scss | 1 + ui/v2.5/src/locales/en-GB.json | 4 + 6 files changed, 316 insertions(+), 130 deletions(-) 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/ScanDialog/styles.scss diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx new file mode 100644 index 00000000000..37ffdf20ad9 --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx @@ -0,0 +1,108 @@ +import React from "react"; +import { Form } from "react-bootstrap"; +import * as GQL from "src/core/generated-graphql"; +import { useIntl } from "react-intl"; + +interface IScanOptionsEditor { + options: GQL.ScanMetadataInput; + setOptions: (s: GQL.ScanMetadataInput) => void; +} + +export const ScanOptions: React.FC = ({ + options, + setOptions: setOptionsState, +}) => { + const intl = useIntl(); + + const { + useFileMetadata, + stripFileExtension, + scanGeneratePreviews, + scanGenerateImagePreviews, + scanGenerateSprites, + scanGeneratePhashes, + scanGenerateThumbnails, + } = options; + + function setOptions(input: Partial) { + setOptionsState({ ...options, ...input }); + } + + return ( + + setOptions({ useFileMetadata: !useFileMetadata })} + /> + setOptions({ stripFileExtension: !stripFileExtension })} + /> + + setOptions({ scanGeneratePreviews: !scanGeneratePreviews }) + } + /> +
+
+ + setOptions({ + scanGenerateImagePreviews: !scanGenerateImagePreviews, + }) + } + className="ml-2 flex-grow" + /> +
+ + setOptions({ scanGenerateSprites: !scanGenerateSprites }) + } + /> + + setOptions({ scanGeneratePhashes: !scanGeneratePhashes }) + } + /> + + setOptions({ scanGenerateThumbnails: !scanGenerateThumbnails }) + } + /> +
+ ); +}; diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx new file mode 100644 index 00000000000..f684b13506b --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx @@ -0,0 +1,194 @@ +import React, { useState, useMemo } from "react"; +import { Button, Form } from "react-bootstrap"; +import { + mutateMetadataScan, + 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 { FormattedMessage, useIntl } from "react-intl"; +import { DirectorySelectionDialog } from "src/components/Settings/SettingsTasksPanel/DirectorySelectionDialog"; +import { Manual } from "src/components/Help/Manual"; +import { ScanOptions } from "./Options"; + +interface IScanDialogProps { + onClose: () => void; +} + +export const ScanDialog: React.FC = ({ onClose }) => { + // TODO - add setting defaults + // 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 { data: configData, error: configError } = useConfiguration(); + + 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 makeDefaultScanInput() { + // const ret = options; + // const { paths: _paths, ...withoutSpecifics } = ret; + // return withoutSpecifics; + // } + + async function onScan() { + try { + await mutateMetadataScan(options); + + 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(), + // }, + // }, + // }); + // } 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={undefined} + // + leftFooterButtons={ + + } + > +
+ {selectionStatus} + setOptions(o)} /> + +
+ ); +}; + +export default ScanDialog; diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/styles.scss b/ui/v2.5/src/components/Dialogs/ScanDialog/styles.scss new file mode 100644 index 00000000000..7b1c9a61d10 --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/styles.scss @@ -0,0 +1,6 @@ +#selected-scan-folders { + & > div { + display: flex; + justify-content: space-between; + } +} diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index f7f53f387b3..633840a19d0 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -4,7 +4,6 @@ import { Button, Form } from "react-bootstrap"; import { mutateMetadataImport, mutateMetadataClean, - mutateMetadataScan, mutateMetadataAutoTag, mutateMetadataExport, mutateMigrateHashNaming, @@ -21,6 +20,7 @@ import { GenerateButton } from "./GenerateButton"; import { ImportDialog } from "./ImportDialog"; import { DirectorySelectionDialog } from "./DirectorySelectionDialog"; import { JobTable } from "./JobTable"; +import ScanDialog from "src/components/Dialogs/ScanDialog/ScanDialog"; type Plugin = Pick; type PluginTask = Pick; @@ -41,26 +41,7 @@ export const SettingsTasksPanel: React.FC = () => { 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); @@ -155,38 +136,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() { @@ -373,88 +323,14 @@ export const SettingsTasksPanel: React.FC = () => {
{intl.formatMessage({ id: "library" })}
- -
{intl.formatMessage({ id: "actions.scan" })}
- setUseFileMetadata(!useFileMetadata)} - /> - setStripFileExtension(!stripFileExtension)} - /> - setScanGeneratePreviews(!scanGeneratePreviews)} - /> -
-
- - setScanGenerateImagePreviews(!scanGenerateImagePreviews) - } - className="ml-2 flex-grow" - /> -
- setScanGenerateSprites(!scanGenerateSprites)} - /> - setScanGeneratePhashes(!scanGeneratePhashes)} - /> - setScanGenerateThumbnails(!scanGenerateThumbnails)} - /> -
- {intl.formatMessage({ id: "config.tasks.scan_for_content_desc" })} @@ -462,9 +338,6 @@ export const SettingsTasksPanel: React.FC = () => { -
- -
+
+ + + ); + }, [intl, paths]); + + if (configError) return
{configError}
; + if (!configData) return
; + + // function makeDefaultScanInput() { + // const ret = options; + // const { paths: _paths, ...withoutSpecifics } = ret; + // return withoutSpecifics; + // } + + async function onAutoTag() { + try { + await mutateMetadataAutoTag(options); + + 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(), + // }, + // }, + // }); + // } 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={undefined} + // + leftFooterButtons={ + + } + > +
+ {selectionStatus} + setOptions(o)} /> + +
+ ); +}; + +export default AutoTagDialog; diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index f4e91b4ea55..e89b227360d 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -159,7 +159,7 @@ export const IdentifyDialog: React.FC = ({ } return ( - +
{message}
diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss b/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss index 9257d0b1d41..450b31e868d 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/styles.scss @@ -36,10 +36,3 @@ .field-options-table td:first-child { padding-left: 0.75rem; } - -#selected-identify-folders { - & > div { - display: flex; - justify-content: space-between; - } -} diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx index 37ffdf20ad9..a5426d3a7f3 100644 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/Options.tsx @@ -3,12 +3,12 @@ import { Form } from "react-bootstrap"; import * as GQL from "src/core/generated-graphql"; import { useIntl } from "react-intl"; -interface IScanOptionsEditor { +interface IScanOptions { options: GQL.ScanMetadataInput; setOptions: (s: GQL.ScanMetadataInput) => void; } -export const ScanOptions: React.FC = ({ +export const ScanOptions: React.FC = ({ options, setOptions: setOptionsState, }) => { diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx index f684b13506b..cb496cacc0a 100644 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx @@ -55,7 +55,7 @@ export const ScanDialog: React.FC = ({ onClose }) => { } return ( - +
{message}
diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/styles.scss b/ui/v2.5/src/components/Dialogs/styles.scss similarity index 52% rename from ui/v2.5/src/components/Dialogs/ScanDialog/styles.scss rename to ui/v2.5/src/components/Dialogs/styles.scss index 7b1c9a61d10..36c350c2d75 100644 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/styles.scss +++ b/ui/v2.5/src/components/Dialogs/styles.scss @@ -1,4 +1,6 @@ -#selected-scan-folders { +@import "IdentifyDialog/styles.scss"; + +.dialog-selected-folders { & > div { display: flex; justify-content: space-between; diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 633840a19d0..c63605cfc6c 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -4,7 +4,6 @@ import { Button, Form } from "react-bootstrap"; import { mutateMetadataImport, mutateMetadataClean, - mutateMetadataAutoTag, mutateMetadataExport, mutateMigrateHashNaming, usePlugins, @@ -18,9 +17,9 @@ import { downloadFile } 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"; type Plugin = Pick; type PluginTask = Pick; @@ -42,9 +41,6 @@ export const SettingsTasksPanel: React.FC = () => { const [isBackupRunning, setIsBackupRunning] = useState(false); const [cleanDryRun, setCleanDryRun] = useState(false); - const [autoTagPerformers, setAutoTagPerformers] = useState(true); - const [autoTagStudios, setAutoTagStudios] = useState(true); - const [autoTagTags, setAutoTagTags] = useState(true); const plugins = usePlugins(); @@ -144,7 +140,7 @@ export const SettingsTasksPanel: React.FC = () => { return; } - return ; + return setDialogOpen({ autoTag: false })} />; } function maybeRenderIdentifyDialog() { @@ -155,38 +151,6 @@ export const SettingsTasksPanel: React.FC = () => { ); } - function onAutoTagDialogClosed(paths?: string[]) { - if (paths) { - onAutoTag(paths); - } - - 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); - } - } - async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) { await mutateRunPluginTask(plugin.id, operation.name); Toast.success({ @@ -352,50 +316,19 @@ export const SettingsTasksPanel: React.FC = () => { -
{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.auto_tag_based_on_filenames", + })} +
diff --git a/ui/v2.5/src/index.scss b/ui/v2.5/src/index.scss index 2d0645f0a04..f7eac74a767 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -21,7 +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/ScanDialog/styles.scss"; +@import "src/components/Dialogs/styles.scss"; /* stylelint-disable */ #root { diff --git a/ui/v2.5/src/locales/de-DE.json b/ui/v2.5/src/locales/de-DE.json index 6a835507c91..e81de6fa885 100644 --- a/ui/v2.5/src/locales/de-DE.json +++ b/ui/v2.5/src/locales/de-DE.json @@ -64,8 +64,6 @@ "search": "Suchen", "select_all": "Alle 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 0ef52796153..05e83f3ee01 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -70,8 +70,6 @@ "search": "Search", "select_all": "Select All", "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…", @@ -293,6 +291,10 @@ }, "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", diff --git a/ui/v2.5/src/locales/es-ES.json b/ui/v2.5/src/locales/es-ES.json index c99633174eb..498ce325485 100644 --- a/ui/v2.5/src/locales/es-ES.json +++ b/ui/v2.5/src/locales/es-ES.json @@ -67,8 +67,6 @@ "search": "Buscar", "select_all": "Seleccionar todo", "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 3a74c7259be..ea579135272 100644 --- a/ui/v2.5/src/locales/it-IT.json +++ b/ui/v2.5/src/locales/it-IT.json @@ -67,8 +67,6 @@ "search": "Cerca", "select_all": "Seleziona Tutto", "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 d7cbc5cd496..5d09ee2fe1a 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 c0fdcc8829d..3ebe809dbe6 100644 --- a/ui/v2.5/src/locales/sv-SE.json +++ b/ui/v2.5/src/locales/sv-SE.json @@ -64,8 +64,6 @@ "search": "Sök", "select_all": "Välj alla", "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 0b89e15a3f3..edabbcba4ef 100644 --- a/ui/v2.5/src/locales/zh-CN.json +++ b/ui/v2.5/src/locales/zh-CN.json @@ -64,8 +64,6 @@ "search": "搜索", "select_all": "选择所有", "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 26e0203b651..ff417985b6d 100644 --- a/ui/v2.5/src/locales/zh-TW.json +++ b/ui/v2.5/src/locales/zh-TW.json @@ -61,8 +61,6 @@ "search": "搜尋", "select_all": "全選", "select_none": "清除選擇", - "selective_auto_tag": "選擇性套用標籤", - "selective_scan": "選擇性掃描", "set_back_image": "設定背面圖…", "set_front_image": "設定正面圖…", "set_image": "設定圖像…", From fa2bf6d845e95ead53790f463453d0f40e95831a Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 3 Nov 2021 17:58:53 +1100 Subject: [PATCH 03/15] Refactor and combine Generate dialog --- .../src/components/Dialogs/GenerateDialog.tsx | 491 ++++++++++++++++++ .../components/Scenes/SceneDetails/Scene.tsx | 4 +- .../components/Scenes/SceneGenerateDialog.tsx | 320 ------------ ui/v2.5/src/components/Scenes/SceneList.tsx | 4 +- .../SettingsTasksPanel/GenerateButton.tsx | 128 ----- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 28 +- ui/v2.5/src/locales/en-GB.json | 4 + 7 files changed, 524 insertions(+), 455 deletions(-) create mode 100644 ui/v2.5/src/components/Dialogs/GenerateDialog.tsx 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/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx new file mode 100644 index 00000000000..8cabf67b4be --- /dev/null +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -0,0 +1,491 @@ +import React, { useState, useEffect, useMemo } 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 { FormattedMessage, useIntl } from "react-intl"; +import { ConfigurationContext } from "src/hooks/Config"; +import { DirectorySelectionDialog } from "../Settings/SettingsTasksPanel/DirectorySelectionDialog"; +import { Manual } from "../Help/Manual"; + +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); + + 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 [paths, setPaths] = useState([]); + const [showManual, setShowManual] = useState(false); + const [settingPaths, setSettingPaths] = useState(false); + const [animation, setAnimation] = useState(true); + + const intl = useIntl(); + const Toast = useToast(); + + useEffect(() => { + if (!configuration) return; + + if (configuration.general) { + 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, + }, + })); + } + }, [configuration]); + + 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(); + } + } + + 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", + }} + > +
+ {selectionStatus} + + +
+ ); +}; diff --git a/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx b/ui/v2.5/src/components/Scenes/SceneDetails/Scene.tsx index dc1fd1b8719..45621544008 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"; @@ -324,7 +324,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 807b7512f92..00000000000 --- a/ui/v2.5/src/components/Scenes/SceneGenerateDialog.tsx +++ /dev/null @@ -1,320 +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: "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 93c223e0324..f94c353cd54 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"; @@ -163,7 +163,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 b251d081d05..00000000000 --- 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 c63605cfc6c..fc6d61d2543 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -15,11 +15,11 @@ import * as GQL from "src/core/generated-graphql"; import { LoadingIndicator, Modal } from "src/components/Shared"; import { downloadFile } from "src/utils"; import IdentifyDialog from "src/components/Dialogs/IdentifyDialog/IdentifyDialog"; -import { GenerateButton } from "./GenerateButton"; import { ImportDialog } from "./ImportDialog"; import { JobTable } from "./JobTable"; import ScanDialog from "src/components/Dialogs/ScanDialog/ScanDialog"; import AutoTagDialog from "src/components/Dialogs/AutoTagDialog"; +import { GenerateDialog } from "src/components/Dialogs/GenerateDialog"; type Plugin = Pick; type PluginTask = Pick; @@ -35,6 +35,7 @@ export const SettingsTasksPanel: React.FC = () => { scan: false, autoTag: false, identify: false, + generate: false, }); type DialogOpenState = typeof dialogOpen; @@ -151,6 +152,14 @@ export const SettingsTasksPanel: React.FC = () => { ); } + function maybeRenderGenerateDialog() { + if (!dialogOpen.generate) return; + + return ( + setDialogOpen({ generate: false })} /> + ); + } + async function onPluginTaskClicked(plugin: Plugin, operation: PluginTask) { await mutateRunPluginTask(plugin.id, operation.name); Toast.success({ @@ -278,6 +287,7 @@ export const SettingsTasksPanel: React.FC = () => { {renderScanDialog()} {renderAutoTagDialog()} {maybeRenderIdentifyDialog()} + {maybeRenderGenerateDialog()}

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

@@ -334,8 +344,20 @@ export const SettingsTasksPanel: React.FC = () => {
-
{intl.formatMessage({ id: "config.tasks.generated_content" })}
- + +
{intl.formatMessage({ id: "config.tasks.generated_content" })}
+ + + {intl.formatMessage({ id: "config.tasks.generate_desc" })} + +

{intl.formatMessage({ id: "config.tasks.maintenance" })}
diff --git a/ui/v2.5/src/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index 05e83f3ee01..ddefed5e531 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -303,6 +303,10 @@ "cleanup_desc": "Check for missing files and remove them from the database. This is a destructive action.", "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)", From 35d5b789501529a486bd895a0c8e0d8a03822976 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 3 Nov 2021 18:24:59 +1100 Subject: [PATCH 04/15] Add clean dialog --- .../src/components/Dialogs/CleanDialog.tsx | 198 ++++++++++++++++++ .../SettingsTasksPanel/SettingsTasksPanel.tsx | 79 ++----- 2 files changed, 217 insertions(+), 60 deletions(-) create mode 100644 ui/v2.5/src/components/Dialogs/CleanDialog.tsx 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 00000000000..8a240093cc8 --- /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/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index fc6d61d2543..2e25ace8a87 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -3,7 +3,6 @@ import { FormattedMessage, useIntl } from "react-intl"; import { Button, Form } from "react-bootstrap"; import { mutateMetadataImport, - mutateMetadataClean, mutateMetadataExport, mutateMigrateHashNaming, usePlugins, @@ -20,6 +19,7 @@ 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"; type Plugin = Pick; type PluginTask = Pick; @@ -29,7 +29,6 @@ export const SettingsTasksPanel: React.FC = () => { const Toast = useToast(); const [dialogOpen, setDialogOpenState] = useState({ importAlert: false, - cleanAlert: false, import: false, clean: false, scan: false, @@ -41,7 +40,6 @@ export const SettingsTasksPanel: React.FC = () => { type DialogOpenState = typeof dialogOpen; const [isBackupRunning, setIsBackupRunning] = useState(false); - const [cleanDryRun, setCleanDryRun] = useState(false); const plugins = usePlugins(); @@ -83,41 +81,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() { @@ -282,7 +251,7 @@ export const SettingsTasksPanel: React.FC = () => { return ( <> {renderImportAlert()} - {renderCleanAlert()} + {renderCleanDialog()} {renderImportDialog()} {renderScanDialog()} {renderAutoTagDialog()} @@ -340,6 +309,19 @@ export const SettingsTasksPanel: React.FC = () => { })} + + + + + {intl.formatMessage({ id: "config.tasks.cleanup_desc" })} + +
@@ -359,29 +341,6 @@ export const SettingsTasksPanel: React.FC = () => { -
-
{intl.formatMessage({ id: "config.tasks.maintenance" })}
- - setCleanDryRun(!cleanDryRun)} - /> - - - - - {intl.formatMessage({ id: "config.tasks.cleanup_desc" })} - - -
{intl.formatMessage({ id: "metadata" })}
From 2dfbaacc0f7e88225ed23d66a263c83a3b110485 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Nov 2021 14:26:36 +1100 Subject: [PATCH 05/15] Styling --- ui/v2.5/src/components/Help/styles.scss | 2 +- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 388 ++++++++++-------- ui/v2.5/src/components/Settings/styles.scss | 33 ++ ui/v2.5/src/index.scss | 8 +- ui/v2.5/src/styles/_theme.scss | 3 +- 5 files changed, 260 insertions(+), 174 deletions(-) diff --git a/ui/v2.5/src/components/Help/styles.scss b/ui/v2.5/src/components/Help/styles.scss index 93f6f39757a..311876ea273 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/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 2e25ace8a87..3d8f204cc42 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, Form } from "react-bootstrap"; +import { Button, ButtonGroup, Card, Form, Row } from "react-bootstrap"; import { mutateMetadataImport, mutateMetadataExport, @@ -146,18 +146,15 @@ export const SettingsTasksPanel: React.FC = () => { return pluginTasks.map((o) => { return ( -
+
+ {o.description} - {o.description ? ( - {o.description} - ) : undefined}
); }); @@ -194,16 +191,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 ?? [])} + +
+ ); + })} +
); } @@ -266,181 +267,232 @@ export const SettingsTasksPanel: React.FC = () => {
{intl.formatMessage({ id: "library" })}
- - - - {intl.formatMessage({ id: "config.tasks.scan_for_content_desc" })} - - - - - - - - - - - - - - {intl.formatMessage({ - id: "config.tasks.auto_tag_based_on_filenames", - })} - - - - - - - {intl.formatMessage({ id: "config.tasks.cleanup_desc" })} - - + +
+ + {intl.formatMessage({ id: "config.tasks.scan_for_content_desc" })} + + + + + +
+ +
+ + {intl.formatMessage({ id: "config.tasks.identify.description" })} + + + + + +
+ +
+ + {intl.formatMessage({ + id: "config.tasks.auto_tag_based_on_filenames", + })} + + + + + +
+ +
+ + {intl.formatMessage({ id: "config.tasks.cleanup_desc" })} + + +
+

{intl.formatMessage({ id: "config.tasks.generated_content" })}
- - - {intl.formatMessage({ id: "config.tasks.generate_desc" })} - -
-
- -
{intl.formatMessage({ id: "metadata" })}
- - - - {intl.formatMessage({ id: "config.tasks.export_to_json" })} - + +
+ + {intl.formatMessage({ id: "config.tasks.generate_desc" })} + + + + + +
+
- - - - {intl.formatMessage({ id: "config.tasks.import_from_exported_json" })} - - +
- - - {intl.formatMessage({ id: "config.tasks.incremental_import" })} - +
{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" })} - +
{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" })}
+ + +
+ + {intl.formatMessage({ id: "config.tasks.migrate_hash_files" })} + + +
+
); diff --git a/ui/v2.5/src/components/Settings/styles.scss b/ui/v2.5/src/components/Settings/styles.scss index 04ace918d1e..96df4cdfc4a 100644 --- a/ui/v2.5/src/components/Settings/styles.scss +++ b/ui/v2.5/src/components/Settings/styles.scss @@ -161,3 +161,36 @@ } } } + +.card.task-group { + padding-bottom: 0.5rem; + padding-top: 0.5rem; +} + +.task-row { + align-items: center; + display: flex; + justify-content: space-between; + padding-bottom: 0.5rem; + padding-top: 0.5rem; + + &:not(:last-child) { + border-bottom: 1px solid $dark-gray2; + } + + :last-child { + margin-left: 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/index.scss b/ui/v2.5/src/index.scss index f7eac74a767..088fc2a19f0 100755 --- a/ui/v2.5/src/index.scss +++ b/ui/v2.5/src/index.scss @@ -376,7 +376,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); @@ -584,7 +584,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; } @@ -602,7 +602,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; } @@ -624,7 +624,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/styles/_theme.scss b/ui/v2.5/src/styles/_theme.scss index ec56fbfcda3..03f7e8b8cea 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; } From 1d9a97bda9269d5affdbb2fc41318bc2615b9874 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Nov 2021 14:51:02 +1100 Subject: [PATCH 06/15] Lint --- .../Settings/SettingsTasksPanel/SettingsTasksPanel.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 3d8f204cc42..b39c1dad51a 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, ButtonGroup, Card, Form, Row } from "react-bootstrap"; +import { Button, ButtonGroup, Card, Form } from "react-bootstrap"; import { mutateMetadataImport, mutateMetadataExport, From 1e526531c2e225330149e51d4212e9a14ebdd1c8 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Nov 2021 15:56:09 +1100 Subject: [PATCH 07/15] Adjust auto tag dialog size --- ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx index 10209bf1c80..1adc17cb58b 100644 --- a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx @@ -196,7 +196,7 @@ export const AutoTagDialog: React.FC = ({ onClose }) => { return ( Date: Fri, 5 Nov 2021 16:24:35 +1100 Subject: [PATCH 08/15] Support scan task default setting --- graphql/documents/data/config.graphql | 10 +++ graphql/schema/types/config.graphql | 2 + graphql/schema/types/metadata.graphql | 18 ++++- pkg/api/resolver_mutation_configure.go | 4 + pkg/api/resolver_query_configuration.go | 1 + pkg/manager/config/config.go | 19 +++++ .../Dialogs/IdentifyDialog/IdentifyDialog.tsx | 20 +++-- .../Dialogs/ScanDialog/ScanDialog.tsx | 74 +++++++++---------- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 41 +++++++++- .../src/components/Shared/OperationButton.tsx | 2 +- ui/v2.5/src/locales/en-GB.json | 1 + 11 files changed, 142 insertions(+), 50 deletions(-) diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index 67f8c60c6a1..c40b604a9f6 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -104,6 +104,16 @@ fragment ScraperSourceData on ScraperSource { } fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult { + scan { + useFileMetadata + stripFileExtension + scanGeneratePreviews + scanGenerateImagePreviews + scanGenerateSprites + scanGeneratePhashes + scanGenerateThumbnails + } + identify { sources { source { diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index 213b253dc1e..a64d44f74c7 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -307,6 +307,7 @@ type ConfigScrapingResult { } type ConfigDefaultSettingsResult { + scan: ScanMetadataOptions identify: IdentifyMetadataTaskOptions """If true, delete file checkbox will be checked by default""" @@ -316,6 +317,7 @@ type ConfigDefaultSettingsResult { } input ConfigDefaultSettingsInput { + scan: ScanMetadataInput identify: IdentifyMetadataInput """If true, delete file checkbox will be checked by default""" diff --git a/graphql/schema/types/metadata.graphql b/graphql/schema/types/metadata.graphql index bb2f5643f50..3e4cb019c77 100644 --- a/graphql/schema/types/metadata.graphql +++ b/graphql/schema/types/metadata.graphql @@ -34,7 +34,6 @@ input GeneratePreviewOptionsInput { } input ScanMetadataInput { - paths: [String!] """Set name, date, details from metadata (if present)""" useFileMetadata: Boolean """Strip file extension from title""" @@ -51,6 +50,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! diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index 2071f531daf..5dfbe8ac909 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -361,6 +361,10 @@ 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.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 e5777ef1b17..3e66bcff3eb 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -170,6 +170,7 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult { return &models.ConfigDefaultSettingsResult{ Identify: config.GetDefaultIdentifySettings(), + Scan: config.GetDefaultScanSettings(), DeleteFile: &deleteFileDefault, DeleteGenerated: &deleteGeneratedDefault, } diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 4105c22da62..5a1788822d6 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -146,6 +146,7 @@ const FunscriptOffset = "funscript_offset" // Default settings const ( + DefaultScanSettings = "defaults.scan_task" DefaultIdentifySettings = "defaults.identify_task" DeleteFileDefault = "defaults.delete_file" @@ -925,6 +926,24 @@ 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() + + if viper.IsSet(DefaultScanSettings) { + var ret models.ScanMetadataOptions + if err := viper.UnmarshalKey(DefaultScanSettings, &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/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index e89b227360d..180169aec09 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"; @@ -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={ + footerButtons={ + + + + } leftFooterButtons={ @@ -297,7 +332,7 @@ export const SettingsTasksPanel: React.FC = () => { diff --git a/ui/v2.5/src/components/Shared/OperationButton.tsx b/ui/v2.5/src/components/Shared/OperationButton.tsx index 7cdc831195f..a7c23dcf28f 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/locales/en-GB.json b/ui/v2.5/src/locales/en-GB.json index ddefed5e531..2c0c5c74978 100644 --- a/ui/v2.5/src/locales/en-GB.json +++ b/ui/v2.5/src/locales/en-GB.json @@ -301,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.", + "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": { From c4357616c5e4149cb2a2c923103bac89e5075704 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Nov 2021 16:30:27 +1100 Subject: [PATCH 09/15] Set defaults from config --- .../components/Dialogs/ScanDialog/ScanDialog.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx index 01d21eac3dc..6d4bbc3fee4 100644 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx @@ -1,4 +1,4 @@ -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import { Button, Form } from "react-bootstrap"; import { mutateMetadataScan, @@ -12,6 +12,7 @@ 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"; interface IScanDialogProps { onClose: () => void; @@ -32,6 +33,18 @@ export const ScanDialog: React.FC = ({ onClose }) => { const { data: configData, error: configError } = useConfiguration(); + useEffect(() => { + if (!configData?.configuration.defaults) { + return; + } + + const { scan } = configData.configuration.defaults; + + if (scan) { + setOptions(withoutTypename(scan)); + } + }, [configData]); + const selectionStatus = useMemo(() => { const message = paths.length ? (
From 4ee133d77a0c4af4a8a524f0750ea40b1ff6eae0 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Nov 2021 16:46:16 +1100 Subject: [PATCH 10/15] Support saving auto tag defaults --- graphql/documents/data/config.graphql | 6 ++ graphql/schema/types/config.graphql | 2 + graphql/schema/types/metadata.graphql | 10 ++ pkg/api/resolver_mutation_configure.go | 4 + pkg/api/resolver_query_configuration.go | 1 + pkg/manager/config/config.go | 19 ++++ .../src/components/Dialogs/AutoTagDialog.tsx | 95 +++++++++++-------- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 18 +++- 8 files changed, 115 insertions(+), 40 deletions(-) diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index c40b604a9f6..e0d9356fc11 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -128,6 +128,12 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult { } } + autoTag { + performers + studios + tags + } + deleteFile deleteGenerated } diff --git a/graphql/schema/types/config.graphql b/graphql/schema/types/config.graphql index a64d44f74c7..6f8c64a466c 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -309,6 +309,7 @@ type ConfigScrapingResult { type ConfigDefaultSettingsResult { scan: ScanMetadataOptions identify: IdentifyMetadataTaskOptions + autoTag: AutoTagMetadataOptions """If true, delete file checkbox will be checked by default""" deleteFile: Boolean @@ -319,6 +320,7 @@ type ConfigDefaultSettingsResult { input ConfigDefaultSettingsInput { scan: ScanMetadataInput identify: IdentifyMetadataInput + autoTag: AutoTagMetadataInput """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 3e4cb019c77..655ced54aeb 100644 --- a/graphql/schema/types/metadata.graphql +++ b/graphql/schema/types/metadata.graphql @@ -34,6 +34,7 @@ input GeneratePreviewOptionsInput { } input ScanMetadataInput { + paths: [String!] """Set name, date, details from metadata (if present)""" useFileMetadata: Boolean """Strip file extension from title""" @@ -83,6 +84,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 5dfbe8ac909..75b7f2177b8 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -365,6 +365,10 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C c.Set(config.DefaultScanSettings, input.Scan) } + if input.AutoTag != nil { + c.Set(config.DefaultAutoTagSettings, input.AutoTag) + } + 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 3e66bcff3eb..c89419ca828 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -171,6 +171,7 @@ func makeConfigDefaultsResult() *models.ConfigDefaultSettingsResult { return &models.ConfigDefaultSettingsResult{ Identify: config.GetDefaultIdentifySettings(), Scan: config.GetDefaultScanSettings(), + AutoTag: config.GetDefaultAutoTagSettings(), DeleteFile: &deleteFileDefault, DeleteGenerated: &deleteGeneratedDefault, } diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 5a1788822d6..5839fbbe449 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -148,6 +148,7 @@ const FunscriptOffset = "funscript_offset" const ( DefaultScanSettings = "defaults.scan_task" DefaultIdentifySettings = "defaults.identify_task" + DefaultAutoTagSettings = "defaults.auto_tag_task" DeleteFileDefault = "defaults.delete_file" DeleteGeneratedDefault = "defaults.delete_generated" @@ -944,6 +945,24 @@ func (i *Instance) GetDefaultScanSettings() *models.ScanMetadataOptions { 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() + + if viper.IsSet(DefaultAutoTagSettings) { + var ret models.AutoTagMetadataOptions + if err := viper.UnmarshalKey(DefaultAutoTagSettings, &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/Dialogs/AutoTagDialog.tsx b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx index 1adc17cb58b..a62b9cfa759 100644 --- a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx @@ -1,16 +1,17 @@ -import React, { useState, useMemo } from "react"; +import React, { useState, useMemo, useEffect } from "react"; import { Button, Form } from "react-bootstrap"; import { mutateMetadataAutoTag, useConfiguration, - // useConfigureDefaults, + useConfigureDefaults, } 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"; 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; @@ -66,21 +67,36 @@ interface IAutoTagDialogProps { } export const AutoTagDialog: React.FC = ({ onClose }) => { - // TODO - add setting defaults - // const [configureDefaults] = useConfigureDefaults(); + const [configureDefaults] = useConfigureDefaults(); - const [options, setOptions] = useState({}); + 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 [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 ? (
@@ -122,11 +138,11 @@ export const AutoTagDialog: React.FC = ({ onClose }) => { if (configError) return
{configError}
; if (!configData) return
; - // function makeDefaultScanInput() { - // const ret = options; - // const { paths: _paths, ...withoutSpecifics } = ret; - // return withoutSpecifics; - // } + function makeDefaultAutoTagInput() { + const ret = options; + const { paths: _paths, ...withoutSpecifics } = ret; + return withoutSpecifics; + } async function onAutoTag() { try { @@ -150,22 +166,29 @@ export const AutoTagDialog: React.FC = ({ onClose }) => { setShowManual(true); } - // async function setAsDefault() { - // try { - // setSavingDefaults(true); - // await configureDefaults({ - // variables: { - // input: { - // autoTag: makeDefaultAutoTagInput(), - // }, - // }, - // }); - // } catch (e) { - // Toast.error(e); - // } finally { - // setSavingDefaults(false); - // } - // } + 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 ( @@ -210,17 +233,11 @@ export const AutoTagDialog: React.FC = ({ onClose }) => { variant: "secondary", }} disabled={savingDefaults} - footerButtons={undefined} - // + footerButtons={ + + + + } leftFooterButtons={ From a4a85c9191fff40af0a24f0ac07b198b05ec75fe Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Fri, 5 Nov 2021 17:18:57 +1100 Subject: [PATCH 11/15] Support for generate defaults --- graphql/documents/data/config.graphql | 18 +++ graphql/schema/types/config.graphql | 2 + graphql/schema/types/metadata.graphql | 25 ++++ pkg/api/resolver_mutation_configure.go | 4 + pkg/api/resolver_query_configuration.go | 1 + pkg/manager/config/config.go | 19 +++ .../src/components/Dialogs/GenerateDialog.tsx | 122 ++++++++++++++---- .../Dialogs/ScanDialog/ScanDialog.tsx | 13 +- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 18 ++- 9 files changed, 186 insertions(+), 36 deletions(-) diff --git a/graphql/documents/data/config.graphql b/graphql/documents/data/config.graphql index e0d9356fc11..9c3bf0b47b7 100644 --- a/graphql/documents/data/config.graphql +++ b/graphql/documents/data/config.graphql @@ -134,6 +134,24 @@ fragment ConfigDefaultSettingsData on ConfigDefaultSettingsResult { 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 6f8c64a466c..600d10ef8f6 100644 --- a/graphql/schema/types/config.graphql +++ b/graphql/schema/types/config.graphql @@ -310,6 +310,7 @@ type ConfigDefaultSettingsResult { scan: ScanMetadataOptions identify: IdentifyMetadataTaskOptions autoTag: AutoTagMetadataOptions + generate: GenerateMetadataOptions """If true, delete file checkbox will be checked by default""" deleteFile: Boolean @@ -321,6 +322,7 @@ 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 655ced54aeb..523be8c5ef3 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)""" diff --git a/pkg/api/resolver_mutation_configure.go b/pkg/api/resolver_mutation_configure.go index 75b7f2177b8..75a2cf8a6ec 100644 --- a/pkg/api/resolver_mutation_configure.go +++ b/pkg/api/resolver_mutation_configure.go @@ -369,6 +369,10 @@ func (r *mutationResolver) ConfigureDefaults(ctx context.Context, input models.C 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 c89419ca828..251529b4457 100644 --- a/pkg/api/resolver_query_configuration.go +++ b/pkg/api/resolver_query_configuration.go @@ -172,6 +172,7 @@ func makeConfigDefaultsResult() *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 5839fbbe449..3fce916152d 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -149,6 +149,7 @@ const ( 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" @@ -963,6 +964,24 @@ func (i *Instance) GetDefaultAutoTagSettings() *models.AutoTagMetadataOptions { 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() + + if viper.IsSet(DefaultGenerateSettings) { + var ret models.GenerateMetadataOptions + if err := viper.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/Dialogs/GenerateDialog.tsx b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx index 8cabf67b4be..8674cff5449 100644 --- a/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/GenerateDialog.tsx @@ -1,13 +1,17 @@ import React, { useState, useEffect, useMemo } from "react"; import { Form, Button, Collapse } from "react-bootstrap"; -import { mutateMetadataGenerate } from "src/core/StashService"; -import { Modal, Icon } from "src/components/Shared"; +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 { DirectorySelectionDialog } from "../Settings/SettingsTasksPanel/DirectorySelectionDialog"; import { Manual } from "../Help/Manual"; +import { withoutTypename } from "src/utils"; interface IGenerateOptions { options: GQL.GenerateMetadataInput; @@ -297,6 +301,7 @@ export const GenerateDialog: React.FC = ({ onClose, }) => { const { configuration } = React.useContext(ConfigurationContext); + const [configureDefaults] = useConfigureDefaults(); function getDefaultOptions(): GQL.GenerateMetadataInput { return { @@ -315,18 +320,27 @@ export const GenerateDialog: React.FC = ({ const [options, setOptions] = useState( getDefaultOptions() ); - const [paths, setPaths] = useState([]); + const [configRead, setConfigRead] = useState(false); + const [paths /* , setPaths */] = useState([]); const [showManual, setShowManual] = useState(false); - const [settingPaths, setSettingPaths] = 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 (!configuration) return; + if (configRead) { + return; + } - if (configuration.general) { + 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, @@ -347,8 +361,9 @@ export const GenerateDialog: React.FC = ({ general.previewPreset ?? existing.previewOptions?.previewPreset, }, })); + setConfigRead(true); } - }, [configuration]); + }, [configuration, configRead]); const selectionStatus = useMemo(() => { if (selectedIds) { @@ -401,23 +416,23 @@ export const GenerateDialog: React.FC = ({ ); - function onClick() { - setAnimation(false); - setSettingPaths(true); - } + // function onClick() { + // setAnimation(false); + // setSettingPaths(true); + // } return (
{message} -
+ {/*
-
+
*/}
); @@ -439,22 +454,58 @@ export const GenerateDialog: React.FC = ({ } } - if (settingPaths) { - return ( - { - if (p) { - setPaths(p); - } - setSettingPaths(false); - }} - /> - ); + 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 ( = ({ text: intl.formatMessage({ id: "actions.cancel" }), variant: "secondary", }} + disabled={savingDefaults} + footerButtons={ + + + + } + leftFooterButtons={ + + } >
{selectionStatus} diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx index 6d4bbc3fee4..a5c77615bf5 100644 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx @@ -2,7 +2,6 @@ import React, { useState, useMemo, useEffect } from "react"; import { Button, Form } from "react-bootstrap"; import { mutateMetadataScan, - useConfiguration, useConfigureDefaults, } from "src/core/StashService"; import { Icon, Modal, OperationButton } from "src/components/Shared"; @@ -13,6 +12,7 @@ import { DirectorySelectionDialog } from "src/components/Settings/SettingsTasksP 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; @@ -31,19 +31,19 @@ export const ScanDialog: React.FC = ({ onClose }) => { const intl = useIntl(); const Toast = useToast(); - const { data: configData, error: configError } = useConfiguration(); + const { configuration } = React.useContext(ConfigurationContext); useEffect(() => { - if (!configData?.configuration.defaults) { + if (!configuration?.defaults) { return; } - const { scan } = configData.configuration.defaults; + const { scan } = configuration.defaults; if (scan) { setOptions(withoutTypename(scan)); } - }, [configData]); + }, [configuration]); const selectionStatus = useMemo(() => { const message = paths.length ? ( @@ -83,8 +83,7 @@ export const ScanDialog: React.FC = ({ onClose }) => { ); }, [intl, paths]); - if (configError) return
{configError}
; - if (!configData) return
; + if (!configuration) return
; function makeDefaultScanInput() { const ret = options; diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 2dc4d609eba..8ae9015944e 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -11,6 +11,7 @@ import { mutateMetadataScan, mutateMetadataIdentify, mutateMetadataAutoTag, + mutateMetadataGenerate, } from "src/core/StashService"; import { useToast } from "src/hooks"; import * as GQL from "src/core/generated-graphql"; @@ -292,6 +293,21 @@ export const SettingsTasksPanel: React.FC = () => { } } + 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 ( { From 86c5fd58294f0452af4b7ca9c93750b8ad4eefa8 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Mon, 8 Nov 2021 13:57:31 +1100 Subject: [PATCH 12/15] Revert styling change --- .../SettingsTasksPanel/SettingsTasksPanel.tsx | 167 ++++++++++-------- ui/v2.5/src/components/Settings/styles.scss | 19 +- 2 files changed, 100 insertions(+), 86 deletions(-) diff --git a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx index 8ae9015944e..6b15e34f822 100644 --- a/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx +++ b/ui/v2.5/src/components/Settings/SettingsTasksPanel/SettingsTasksPanel.tsx @@ -25,10 +25,27 @@ 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(); @@ -153,8 +170,7 @@ export const SettingsTasksPanel: React.FC = () => { return pluginTasks.map((o) => { return ( -
- {o.description} + -
+ ); }); } @@ -335,10 +351,11 @@ export const SettingsTasksPanel: React.FC = () => {
{intl.formatMessage({ id: "library" })}
-
- - {intl.formatMessage({ id: "config.tasks.scan_for_content_desc" })} - + -
+ -
- - {intl.formatMessage({ id: "config.tasks.identify.description" })} - + -
- -
- - {intl.formatMessage({ - id: "config.tasks.auto_tag_based_on_filenames", - })} - + + + -
+ -
- - {intl.formatMessage({ id: "config.tasks.cleanup_desc" })} - + -
+
@@ -421,10 +439,11 @@ export const SettingsTasksPanel: React.FC = () => {
{intl.formatMessage({ id: "config.tasks.generated_content" })}
-
- - {intl.formatMessage({ id: "config.tasks.generate_desc" })} - + -
+
@@ -449,10 +468,11 @@ export const SettingsTasksPanel: React.FC = () => {
{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" })} - + -
+
@@ -500,19 +520,18 @@ export const SettingsTasksPanel: React.FC = () => {
{intl.formatMessage({ id: "actions.backup" })}
-
- - {intl.formatMessage( - { id: "config.tasks.backup_database" }, - { - filename_format: ( - - [origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] - - ), - } - )} - + + [origFilename].sqlite.[schemaVersion].[YYYYMMDD_HHMMSS] + + ), + } + )} + > -
+ -
- - {intl.formatMessage({ id: "config.tasks.backup_and_download" })} - + -
+
@@ -547,10 +567,11 @@ export const SettingsTasksPanel: React.FC = () => {
{intl.formatMessage({ id: "config.tasks.migrations" })}
-
- - {intl.formatMessage({ id: "config.tasks.migrate_hash_files" })} - + -
+
diff --git a/ui/v2.5/src/components/Settings/styles.scss b/ui/v2.5/src/components/Settings/styles.scss index 96df4cdfc4a..eb1fe2e51cb 100644 --- a/ui/v2.5/src/components/Settings/styles.scss +++ b/ui/v2.5/src/components/Settings/styles.scss @@ -165,21 +165,14 @@ .card.task-group { padding-bottom: 0.5rem; padding-top: 0.5rem; -} - -.task-row { - align-items: center; - display: flex; - justify-content: space-between; - padding-bottom: 0.5rem; - padding-top: 0.5rem; - &:not(:last-child) { - border-bottom: 1px solid $dark-gray2; - } + .task { + padding-bottom: 0.5rem; + padding-top: 0.5rem; - :last-child { - margin-left: 1rem; + &:not(:last-child) { + border-bottom: 1px solid $dark-gray2; + } } } From 7ca3d36cb6ae6b09a397dccfcafb4b7ccfa4b01d Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 23 Nov 2021 08:27:42 +1100 Subject: [PATCH 13/15] Fix config settings --- pkg/manager/config/config.go | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/pkg/manager/config/config.go b/pkg/manager/config/config.go index 6bdbe4ed5fc..bfe1b700305 100644 --- a/pkg/manager/config/config.go +++ b/pkg/manager/config/config.go @@ -963,10 +963,11 @@ func (i *Instance) GetDefaultIdentifySettings() *models.IdentifyMetadataTaskOpti func (i *Instance) GetDefaultScanSettings() *models.ScanMetadataOptions { i.RLock() defer i.RUnlock() + v := i.viper(DefaultScanSettings) - if viper.IsSet(DefaultScanSettings) { + if v.IsSet(DefaultScanSettings) { var ret models.ScanMetadataOptions - if err := viper.UnmarshalKey(DefaultScanSettings, &ret); err != nil { + if err := v.UnmarshalKey(DefaultScanSettings, &ret); err != nil { return nil } return &ret @@ -981,10 +982,11 @@ func (i *Instance) GetDefaultScanSettings() *models.ScanMetadataOptions { func (i *Instance) GetDefaultAutoTagSettings() *models.AutoTagMetadataOptions { i.RLock() defer i.RUnlock() + v := i.viper(DefaultAutoTagSettings) - if viper.IsSet(DefaultAutoTagSettings) { + if v.IsSet(DefaultAutoTagSettings) { var ret models.AutoTagMetadataOptions - if err := viper.UnmarshalKey(DefaultAutoTagSettings, &ret); err != nil { + if err := v.UnmarshalKey(DefaultAutoTagSettings, &ret); err != nil { return nil } return &ret @@ -999,10 +1001,11 @@ func (i *Instance) GetDefaultAutoTagSettings() *models.AutoTagMetadataOptions { func (i *Instance) GetDefaultGenerateSettings() *models.GenerateMetadataOptions { i.RLock() defer i.RUnlock() + v := i.viper(DefaultGenerateSettings) - if viper.IsSet(DefaultGenerateSettings) { + if v.IsSet(DefaultGenerateSettings) { var ret models.GenerateMetadataOptions - if err := viper.UnmarshalKey(DefaultGenerateSettings, &ret); err != nil { + if err := v.UnmarshalKey(DefaultGenerateSettings, &ret); err != nil { return nil } return &ret From 841ba2475d58124bd788ddf4ed95c6491f44d3ff Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Tue, 23 Nov 2021 12:50:27 +1100 Subject: [PATCH 14/15] Fix UI issues --- ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx | 7 +++++-- ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx | 5 ++++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx index a62b9cfa759..960467463a6 100644 --- a/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/AutoTagDialog.tsx @@ -28,7 +28,7 @@ const AutoTagOptions: React.FC = ({ const wildcard = ["*"]; function toggle(v?: GQL.Maybe) { - if (!v) { + if (!v?.length) { return wildcard; } return []; @@ -146,7 +146,10 @@ export const AutoTagDialog: React.FC = ({ onClose }) => { async function onAutoTag() { try { - await mutateMetadataAutoTag(options); + await mutateMetadataAutoTag({ + ...options, + paths: paths.length ? paths : undefined, + }); Toast.success({ content: intl.formatMessage( diff --git a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx index a5c77615bf5..da5d927ae7d 100644 --- a/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/ScanDialog/ScanDialog.tsx @@ -93,7 +93,10 @@ export const ScanDialog: React.FC = ({ onClose }) => { async function onScan() { try { - await mutateMetadataScan(options); + await mutateMetadataScan({ + ...options, + paths: paths.length ? paths : undefined, + }); Toast.success({ content: intl.formatMessage( From 862c9a24e79c21b537dd5419e86d6abd8c61d278 Mon Sep 17 00:00:00 2001 From: WithoutPants <53250216+WithoutPants@users.noreply.github.com> Date: Wed, 24 Nov 2021 09:08:59 +1100 Subject: [PATCH 15/15] Add changelog entry [skip ci] --- ui/v2.5/src/components/Changelog/versions/v0120.md | 1 + 1 file changed, 1 insertion(+) diff --git a/ui/v2.5/src/components/Changelog/versions/v0120.md b/ui/v2.5/src/components/Changelog/versions/v0120.md index d4d67339c59..50c5dd9feda 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))