diff --git a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx index ece7589dcfb..2d4565b9091 100644 --- a/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx +++ b/ui/v2.5/src/components/Dialogs/IdentifyDialog/IdentifyDialog.tsx @@ -26,10 +26,9 @@ import { faCogs, faFolderOpen, faQuestionCircle, + faTriangleExclamation, } from "@fortawesome/free-solid-svg-icons"; -const autoTagScraperID = "builtin_autotag"; - interface IIdentifyDialogProps { selectedIds?: string[]; onClose: () => void; @@ -228,28 +227,11 @@ export const IdentifyDialog: React.FC = ({ // default to first stash-box instance only const stashBox = allSources.find((s) => s.stash_box_endpoint); - // add auto-tag as well - const autoTag = allSources.find( - (s) => s.id === `${SCRAPER_PREFIX}${autoTagScraperID}` - ); - const newSources: IScraperSource[] = []; if (stashBox) { newSources.push(stashBox); } - // sanity check - this should always be true - if (autoTag) { - // don't set organised by default - const autoTagCopy = { ...autoTag }; - autoTagCopy.options = { - setOrganized: false, - skipMultipleMatches: true, - skipSingleNamePerformers: true, - }; - newSources.push(autoTagCopy); - } - setSources(newSources); } }, [allSources, configData]); @@ -445,6 +427,12 @@ export const IdentifyDialog: React.FC = ({ } > +
+

+ + +

+
{selectionStatus} = ({ id: "config.tasks.identify.set_organized", })} defaultValue={defaultOptions?.setOrganized ?? undefined} + tooltip={intl.formatMessage({ + id: "config.tasks.identify.organized", + })} {...checkboxProps} /> diff --git a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx index cbcca94a161..71d20098781 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DataManagementTasks.tsx @@ -1,6 +1,6 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; -import { Button, Col, Form, Row } from "react-bootstrap"; +import { Button, Form } from "react-bootstrap"; import { mutateMigrateHashNaming, mutateMetadataExport, @@ -21,110 +21,11 @@ import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting } from "../Inputs"; import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared/Icon"; -import { ConfigurationContext } from "src/hooks/Config"; -import { FolderSelect } from "src/components/Shared/FolderSelect/FolderSelect"; import { - faMinus, - faPlus, faQuestionCircle, faTrashAlt, } from "@fortawesome/free-solid-svg-icons"; - -interface ICleanDialog { - pathSelection?: boolean; - dryRun: boolean; - onClose: (paths?: string[]) => void; -} - -const CleanDialog: React.FC = ({ - pathSelection = false, - dryRun, - onClose, -}) => { - const intl = useIntl(); - const { configuration } = React.useContext(ConfigurationContext); - - const libraryPaths = configuration?.general.stashes.map((s) => s.path); - - const [paths, setPaths] = useState([]); - const [currentDirectory, setCurrentDirectory] = useState(""); - - function removePath(p: string) { - setPaths(paths.filter((path) => path !== p)); - } - - function addPath(p: string) { - if (p && !paths.includes(p)) { - setPaths(paths.concat(p)); - } - } - - let msg; - if (dryRun) { - msg = ( -

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

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

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

- ); - } - - return ( - onClose(paths), - }} - cancel={{ onClick: () => onClose() }} - > -
-
- {paths.map((p) => ( - - - {p} - - - - - - ))} - - {pathSelection ? ( - setCurrentDirectory(v)} - defaultDirectories={libraryPaths} - appendButton={ - - } - /> - ) : undefined} -
- - {msg} -
-
- ); -}; +import { DirectorySelectionDialog } from "./DirectorySelectionDialog"; interface ICleanOptions { options: GQL.CleanMetadataInput; @@ -232,6 +133,45 @@ export const DataManagementTasks: React.FC = ({ return setDialogOpen({ import: false })} />; } + function renderCleanDialog() { + if (dialogOpen.cleanAlert || dialogOpen.clean) { + let msg: string; + if (cleanOptions.dryRun) { + msg = intl.formatMessage({ id: "actions.tasks.dry_mode_selected" }); + } else { + msg = intl.formatMessage({ id: "actions.tasks.clean_confirm_message" }); + } + + return ( + { + // undefined means cancelled + if (p !== undefined) { + if (dialogOpen.cleanAlert) { + // don't provide paths + onClean(); + } else { + onClean(p); + } + } + + setDialogOpen({ + clean: false, + cleanAlert: false, + }); + }} + /> + ); + } + return; + } + async function onClean(paths?: string[]) { try { await mutateMetadataClean({ @@ -380,30 +320,7 @@ export const DataManagementTasks: React.FC = ({ {renderImportAlert()} {renderImportDialog()} - {dialogOpen.cleanAlert || dialogOpen.clean ? ( - { - // undefined means cancelled - if (p !== undefined) { - if (dialogOpen.cleanAlert) { - // don't provide paths - onClean(); - } else { - onClean(p); - } - } - - setDialogOpen({ - clean: false, - cleanAlert: false, - }); - }} - /> - ) : ( - dialogOpen.clean - )} + {renderCleanDialog()}
diff --git a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx index 30e64ebcdf5..1940fa64767 100644 --- a/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/DirectorySelectionDialog.tsx @@ -1,4 +1,5 @@ import { + IconDefinition, faMinus, faPencilAlt, faPlus, @@ -15,12 +16,29 @@ interface IDirectorySelectionDialogProps { animation?: boolean; initialPaths?: string[]; allowEmpty?: boolean; + allowPathSelection?: boolean; + message?: string | React.ReactNode; + header?: string; + icon?: IconDefinition; + acceptButtonText?: string; + acceptButtonVariant?: "danger" | "primary" | "secondary"; onClose: (paths?: string[]) => void; } export const DirectorySelectionDialog: React.FC< IDirectorySelectionDialogProps -> = ({ animation, allowEmpty = false, initialPaths = [], onClose }) => { +> = ({ + animation, + allowEmpty = false, + initialPaths = [], + allowPathSelection = true, + message, + header, + icon = faPencilAlt, + acceptButtonText, + acceptButtonVariant = "primary", + onClose, +}) => { const intl = useIntl(); const { configuration } = React.useContext(ConfigurationContext); @@ -43,14 +61,15 @@ export const DirectorySelectionDialog: React.FC< { onClose(paths); }, - text: intl.formatMessage({ id: "actions.confirm" }), + text: acceptButtonText ?? intl.formatMessage({ id: "actions.confirm" }), + variant: acceptButtonVariant, }} cancel={{ onClick: () => onClose(), @@ -78,19 +97,22 @@ export const DirectorySelectionDialog: React.FC< ))} - setCurrentDirectory(v)} - defaultDirectories={libraryPaths} - appendButton={ - - } - /> + {allowPathSelection ? ( + setCurrentDirectory(v)} + defaultDirectories={libraryPaths} + appendButton={ + + } + /> + ) : undefined} + {message}
); diff --git a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx index 6069ef433a0..0c861f744f3 100644 --- a/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx +++ b/ui/v2.5/src/components/Settings/Tasks/LibraryTasks.tsx @@ -19,7 +19,10 @@ import { SettingSection } from "../SettingSection"; import { BooleanSetting, Setting, SettingGroup } from "../Inputs"; import { ManualLink } from "src/components/Help/context"; import { Icon } from "src/components/Shared/Icon"; -import { faQuestionCircle } from "@fortawesome/free-solid-svg-icons"; +import { + faQuestionCircle, + faTriangleExclamation, +} from "@fortawesome/free-solid-svg-icons"; interface IAutoTagOptions { options: GQL.AutoTagMetadataInput; @@ -74,17 +77,19 @@ export const LibraryTasks: React.FC = () => { const [configureDefaults] = useConfigureDefaults(); const [dialogOpen, setDialogOpenState] = useState({ - clean: false, scan: false, autoTag: false, + autoTagAlert: false, identify: false, }); const [scanOptions, setScanOptions] = useState({}); const [autoTagOptions, setAutoTagOptions] = useState({ - performers: ["*"], - studios: ["*"], + // Updated per issue #3909 so that new users don't accidentally + // auto tag their entire library with performers and studios. + performers: [], + studios: [], tags: ["*"], }); @@ -210,19 +215,44 @@ export const LibraryTasks: React.FC = () => { } function renderAutoTagDialog() { - if (!dialogOpen.autoTag) { - return; - } - - return ; - } - - function onAutoTagDialogClosed(paths?: string[]) { - if (paths) { - runAutoTag(paths); + if (dialogOpen.autoTagAlert || dialogOpen.autoTag) { + return ( + +

+ {intl.formatMessage({ + id: "config.tasks.auto_tag_based_on_filenames", + })} +

+ + {intl.formatMessage({ id: "config.tasks.auto_tag_warning" })} + + } + header={intl.formatMessage({ id: "actions.auto_tag" })} + icon={faTriangleExclamation} + acceptButtonText={intl.formatMessage({ id: "actions.continue" })} + acceptButtonVariant="danger" + onClose={(p) => { + // undefined means cancelled + if (p !== undefined) { + if (dialogOpen.autoTagAlert) { + // don't provide paths + runAutoTag(); + } else { + runAutoTag(p); + } + } + + setDialogOpen({ + autoTag: false, + autoTagAlert: false, + }); + }} + /> + ); } - - setDialogOpen({ autoTag: false }); } async function runAutoTag(paths?: string[]) { @@ -337,7 +367,14 @@ export const LibraryTasks: React.FC = () => { } - subHeadingID="config.tasks.identify.description" + subHeading={ + <> + {intl.formatMessage({ id: "config.tasks.identify.description" })} +
+ + {intl.formatMessage({ id: "config.tasks.identify.warning" })} + + } > diff --git a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx index fb5646ff571..a08cf756fde 100644 --- a/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx +++ b/ui/v2.5/src/components/Shared/DetailsEditNavbar.tsx @@ -3,6 +3,8 @@ import React, { useState } from "react"; import { FormattedMessage, useIntl } from "react-intl"; import { ImageInput } from "./ImageInput"; import cx from "classnames"; +import { Icon } from "./Icon"; +import { faTriangleExclamation } from "@fortawesome/free-solid-svg-icons"; interface IProps { objectName?: string; @@ -28,6 +30,7 @@ interface IProps { export const DetailsEditNavbar: React.FC = (props: IProps) => { const intl = useIntl(); const [isDeleteAlertOpen, setIsDeleteAlertOpen] = useState(false); + const [isAutoTagAlertOpen, setIsAutoTagAlertOpen] = useState(false); function renderEditButton() { if (props.isNew) return; @@ -89,22 +92,58 @@ export const DetailsEditNavbar: React.FC = (props: IProps) => { function renderAutoTagButton() { if (props.isNew || props.isEditing) return; - if (props.onAutoTag) { - return ( -
+ return ( +
+ +
+ ); + } + + function renderAutoTagAlert() { + return ( + + + + + + + + +

+ +

+

+ + +

+ +
    +
  • {props.objectName}
  • +
+
+ -
- ); - } + + + + ); } function renderDeleteAlert() { @@ -171,6 +210,7 @@ export const DetailsEditNavbar: React.FC = (props: IProps) => { ) : null} {renderAutoTagButton()} + {renderAutoTagAlert()} {props.customButtons} {renderSaveButton()} {renderDeleteButton()} diff --git a/ui/v2.5/src/components/Shared/styles.scss b/ui/v2.5/src/components/Shared/styles.scss index 97bf7817f70..50823c92ef4 100644 --- a/ui/v2.5/src/components/Shared/styles.scss +++ b/ui/v2.5/src/components/Shared/styles.scss @@ -100,6 +100,7 @@ .folder-list { list-style-type: none; margin: 0; + padding-bottom: 1rem; padding-top: 1rem; &-item { @@ -367,6 +368,7 @@ div.react-datepicker { .react-datepicker__day { color: $text-color; + &.react-datepicker__day--disabled { color: $text-muted; } @@ -436,6 +438,7 @@ div.react-datepicker { } } } + /* stylelint-enable */ #date-picker-portal .react-datepicker-popper { diff --git a/ui/v2.5/src/components/Tags/TagList.tsx b/ui/v2.5/src/components/Tags/TagList.tsx index 98035f12ec0..c747198b6ed 100644 --- a/ui/v2.5/src/components/Tags/TagList.tsx +++ b/ui/v2.5/src/components/Tags/TagList.tsx @@ -8,7 +8,7 @@ import { PersistanceLevel, showWhenSelected, } from "../List/ItemList"; -import { Button } from "react-bootstrap"; +import { Button, Modal } from "react-bootstrap"; import { Link, useHistory } from "react-router-dom"; import * as GQL from "src/core/generated-graphql"; import { @@ -27,7 +27,10 @@ import { DeleteEntityDialog } from "../Shared/DeleteEntityDialog"; import { TagCard } from "./TagCard"; import { ExportDialog } from "../Shared/ExportDialog"; import { tagRelationHook } from "../../core/tags"; -import { faTrashAlt } from "@fortawesome/free-solid-svg-icons"; +import { + faTrashAlt, + faTriangleExclamation, +} from "@fortawesome/free-solid-svg-icons"; interface ITagList { filterHook?: (filter: ListFilterModel) => ListFilterModel; @@ -63,6 +66,7 @@ export const TagList: React.FC = ({ filterHook, alterQuery }) => { const history = useHistory(); const [isExportDialogOpen, setIsExportDialogOpen] = useState(false); const [isExportAll, setIsExportAll] = useState(false); + const [isAutoTagAlertOpen, setIsAutoTagAlertOpen] = useState(false); const otherOperations = [ { @@ -125,6 +129,7 @@ export const TagList: React.FC = ({ filterHook, alterQuery }) => { } async function onAutoTag(tag: GQL.TagDataFragment) { + setIsAutoTagAlertOpen(false); if (!tag) return; try { await mutateMetadataAutoTag({ tags: [tag.id] }); @@ -185,6 +190,45 @@ export const TagList: React.FC = ({ filterHook, alterQuery }) => { } } + function renderAutoTagButton() { + return ( + + ); + } + + function renderAutoTagAlert(tag: GQL.TagDataFragment) { + return ( + + + + + + + + + + + + + + + + ); + } + function renderTags() { if (!result.data?.findTags) return; @@ -234,13 +278,8 @@ export const TagList: React.FC = ({ filterHook, alterQuery }) => { {tag.name}
- + {renderAutoTagButton()} + {renderAutoTagAlert(tag)}