diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index 5f34bbb2e78..08b095f21ec 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -152,8 +152,8 @@ "orderBy": "Order By", "outpaint": "outpaint", "outputs": "Outputs", - "postprocessing": "Post Processing", - "random": "Random", + "postprocessing": "Post Processing", "random": "Random", + "recall": "Recall", "reportBugLabel": "Report Bug", "safetensors": "Safetensors", "save": "Save", @@ -393,10 +393,11 @@ "compareHelp1": "Hold Alt while clicking a gallery image or using the arrow keys to change the compare image.", "compareHelp2": "Press M to cycle through comparison modes.", "compareHelp3": "Press C to swap the compared images.", - "compareHelp4": "Press Z or Esc to exit.", - "openViewer": "Open Viewer", + "compareHelp4": "Press Z or Esc to exit.", "openViewer": "Open Viewer", "closeViewer": "Close Viewer", - "move": "Move" + "move": "Move", + "recallParametersCanvasWarning": "Recalling parameters will potentially overwrite your current canvas settings and may affect active layers. Some canvas parameters may be overwritten.", + "activeCanvasData": "Active Canvas Data: {{data}}" }, "hotkeys": { "hotkeys": "Hotkeys", diff --git a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx index da982d105f5..2f0b93a25af 100644 --- a/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx +++ b/invokeai/frontend/web/src/app/components/GlobalModalIsolator.tsx @@ -11,6 +11,7 @@ import { FullscreenDropzone } from 'features/dnd/FullscreenDropzone'; import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal'; import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal'; import { ImageContextMenu } from 'features/gallery/components/ImageContextMenu/ImageContextMenu'; +import { RecallMetadataConfirmationAlertDialog } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog'; import { ShareWorkflowModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/ShareWorkflowModal'; import { WorkflowLibraryModal } from 'features/nodes/components/sidePanel/workflow/WorkflowLibrary/WorkflowLibraryModal'; import { CancelAllExceptCurrentQueueItemConfirmationAlertDialog } from 'features/queue/components/CancelAllExceptCurrentQueueItemConfirmationAlertDialog'; @@ -47,9 +48,9 @@ export const GlobalModalIsolator = memo(() => { - - + + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx index 3df7be7e04e..4195fb37d72 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemMetadataRecallActions.tsx @@ -1,8 +1,9 @@ import { Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { SubMenuButtonContent, useSubMenu } from 'common/hooks/useSubMenu'; +import { useRecallMetadataWithConfirmation } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog'; import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowBendUpLeftBold, @@ -17,10 +18,19 @@ export const ImageMenuItemMetadataRecallActions = memo(() => { const { t } = useTranslation(); const imageDTO = useImageDTOContext(); const subMenu = useSubMenu(); + const { recallWithConfirmation } = useRecallMetadataWithConfirmation(); const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, createAsPreset } = useImageActions(imageDTO); + const handleRecallAll = useCallback(() => { + if (hasMetadata) { + recallWithConfirmation(() => { + recallAll(); + }); + } + }, [hasMetadata, recallAll, recallWithConfirmation]); + return ( }> @@ -36,8 +46,7 @@ export const ImageMenuItemMetadataRecallActions = memo(() => { } onClick={recallSeed} isDisabled={!hasSeed}> {t('parameters.useSeed')} - - } onClick={recallAll} isDisabled={!hasMetadata}> + } onClick={handleRecallAll} isDisabled={!hasMetadata}> {t('parameters.useAll')} } onClick={createAsPreset} isDisabled={!hasPrompts}> diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageHoverIcons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageHoverIcons.tsx index dcaa5729d13..b221116555b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageHoverIcons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageHoverIcons.tsx @@ -1,6 +1,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { GalleryImageDeleteIconButton } from 'features/gallery/components/ImageGrid/GalleryImageDeleteIconButton'; import { GalleryImageOpenInViewerIconButton } from 'features/gallery/components/ImageGrid/GalleryImageOpenInViewerIconButton'; +import { GalleryImageRecallAllIconButton } from 'features/gallery/components/ImageGrid/GalleryImageRecallAllIconButton'; import { GalleryImageSizeBadge } from 'features/gallery/components/ImageGrid/GalleryImageSizeBadge'; import { GalleryImageStarIconButton } from 'features/gallery/components/ImageGrid/GalleryImageStarIconButton'; import { selectAlwaysShouldImageSizeBadge } from 'features/gallery/store/gallerySelectors'; @@ -14,11 +15,11 @@ type Props = { export const GalleryImageHoverIcons = memo(({ imageDTO, isHovered }: Props) => { const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge); - return ( <> {(isHovered || alwaysShowImageSizeBadge) && } {(isHovered || imageDTO.starred) && } + {isHovered && } {isHovered && } {isHovered && } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageRecallAllIconButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageRecallAllIconButton.tsx new file mode 100644 index 00000000000..e1bd23fb901 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImageRecallAllIconButton.tsx @@ -0,0 +1,38 @@ +import { DndImageIcon } from 'features/dnd/DndImageIcon'; +import { useRecallMetadataWithConfirmation } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog'; +import { useImageActions } from 'features/gallery/hooks/useImageActions'; +import { memo, useCallback } from 'react'; +import { PiAsteriskBold } from 'react-icons/pi'; +import type { ImageDTO } from 'services/api/types'; + +type Props = { + imageDTO: ImageDTO; +}; + +export const GalleryImageRecallAllIconButton = memo(({ imageDTO }: Props) => { + const imageActions = useImageActions(imageDTO); + const { recallWithConfirmation } = useRecallMetadataWithConfirmation(); + + const onClick = useCallback(() => { + if (imageActions.hasMetadata) { + recallWithConfirmation(() => { + imageActions.recallAll(); + }); + } + }, [imageActions, recallWithConfirmation]); + + return ( + } + tooltip="Recall" + position="absolute" + insetBlockStart={2} + insetInlineStart="50%" + transform="translateX(-50%)" + isDisabled={!imageActions.hasMetadata} + /> + ); +}); + +GalleryImageRecallAllIconButton.displayName = 'GalleryImageRecallAllIconButton'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog.tsx new file mode 100644 index 00000000000..966cd7b24c6 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog.tsx @@ -0,0 +1,148 @@ +import { Checkbox, ConfirmationAlertDialog, Flex, FormControl, FormLabel, Text } from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useAssertSingleton } from 'common/hooks/useAssertSingleton'; +import { buildUseBoolean } from 'common/hooks/useBoolean'; +import { + selectActiveControlLayerEntities, + selectActiveInpaintMaskEntities, + selectActiveRasterLayerEntities, + selectActiveReferenceImageEntities, + selectActiveRegionalGuidanceEntities, +} from 'features/controlLayers/store/selectors'; +import { + selectSystemShouldConfirmOnNewSession, + shouldConfirmOnNewSessionToggled, +} from 'features/system/store/systemSlice'; +import { memo, useCallback, useMemo } from 'react'; +import { useTranslation } from 'react-i18next'; + +type RecallMetadataCallback = () => void; + +const [useRecallMetadataConfirmationDialog] = buildUseBoolean(false); + +let pendingRecallCallback: RecallMetadataCallback | null = null; + +export const useRecallMetadataWithConfirmation = () => { + const dialog = useRecallMetadataConfirmationDialog(); + const shouldConfirm = useAppSelector(selectSystemShouldConfirmOnNewSession); + + // Check if there are any active canvas layers that would be affected + const activeRasterLayers = useAppSelector(selectActiveRasterLayerEntities); + const activeControlLayers = useAppSelector(selectActiveControlLayerEntities); + const activeInpaintMasks = useAppSelector(selectActiveInpaintMaskEntities); + const activeRegionalGuidance = useAppSelector(selectActiveRegionalGuidanceEntities); + const activeReferenceImages = useAppSelector(selectActiveReferenceImageEntities); + + const hasActiveCanvasData = useMemo(() => { + return ( + activeRasterLayers.length > 0 || + activeControlLayers.length > 0 || + activeInpaintMasks.length > 0 || + activeRegionalGuidance.length > 0 || + activeReferenceImages.length > 0 + ); + }, [ + activeRasterLayers.length, + activeControlLayers.length, + activeInpaintMasks.length, + activeRegionalGuidance.length, + activeReferenceImages.length, + ]); + + const recallWithConfirmation = useCallback( + (recallCallback: RecallMetadataCallback) => { + // If there's no active canvas data or user has disabled confirmations, recall immediately + if (!hasActiveCanvasData || !shouldConfirm) { + recallCallback(); + return; + } + + // Store the callback and show the confirmation dialog + pendingRecallCallback = recallCallback; + dialog.setTrue(); + }, + [dialog, hasActiveCanvasData, shouldConfirm] + ); + + return { + recallWithConfirmation, + hasActiveCanvasData, + }; +}; + +export const RecallMetadataConfirmationAlertDialog = memo(() => { + useAssertSingleton('RecallMetadataConfirmationAlertDialog'); + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const dialog = useRecallMetadataConfirmationDialog(); + const shouldConfirm = useAppSelector(selectSystemShouldConfirmOnNewSession); + + const activeRasterLayers = useAppSelector(selectActiveRasterLayerEntities); + const activeControlLayers = useAppSelector(selectActiveControlLayerEntities); + const activeInpaintMasks = useAppSelector(selectActiveInpaintMaskEntities); + const activeRegionalGuidance = useAppSelector(selectActiveRegionalGuidanceEntities); + const activeReferenceImages = useAppSelector(selectActiveReferenceImageEntities); + + const onConfirm = useCallback(() => { + if (pendingRecallCallback) { + pendingRecallCallback(); + pendingRecallCallback = null; + } + dialog.setFalse(); + }, [dialog]); + + const onCancel = useCallback(() => { + pendingRecallCallback = null; + dialog.setFalse(); + }, [dialog]); + + const onToggleConfirm = useCallback(() => { + dispatch(shouldConfirmOnNewSessionToggled()); + }, [dispatch]); + const getCanvasDataSummary = useCallback(() => { + const items = []; + if (activeRasterLayers.length > 0) { + items.push(t('controlLayers.rasterLayer_withCount_other', { count: activeRasterLayers.length })); + } + if (activeControlLayers.length > 0) { + items.push(t('controlLayers.controlLayer_withCount_other', { count: activeControlLayers.length })); + } + if (activeInpaintMasks.length > 0) { + items.push(t('controlLayers.inpaintMask_withCount_other', { count: activeInpaintMasks.length })); + } + if (activeRegionalGuidance.length > 0) { + items.push(t('controlLayers.regionalGuidance_withCount_other', { count: activeRegionalGuidance.length })); + } + if (activeReferenceImages.length > 0) { + items.push(t('controlLayers.globalReferenceImage_withCount_other', { count: activeReferenceImages.length })); + } + return items.join(', '); + }, [activeRasterLayers.length, activeControlLayers.length, activeInpaintMasks.length, activeRegionalGuidance.length, activeReferenceImages.length, t]); + + return ( + + + {t('gallery.recallParametersCanvasWarning')} + + {t('gallery.activeCanvasData', { data: getCanvasDataSummary() })} + + {t('common.areYouSure')} + + {t('common.dontAskMeAgain')} + + + + + ); +}); + +RecallMetadataConfirmationAlertDialog.displayName = 'RecallMetadataConfirmationAlertDialog'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx index 9bef56ccd20..9080a4f0344 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImageButtons.tsx @@ -5,12 +5,13 @@ import { useAppSelector } from 'app/store/storeHooks'; import { selectIsStaging } from 'features/controlLayers/store/canvasStagingAreaSlice'; import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton'; import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems'; +import { useRecallMetadataWithConfirmation } from 'features/gallery/components/ImageGrid/RecallMetadataConfirmationAlertDialog'; import { useImageActions } from 'features/gallery/hooks/useImageActions'; import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors'; import { $hasTemplates } from 'features/nodes/store/nodesSlice'; import { PostProcessingPopover } from 'features/parameters/components/PostProcessing/PostProcessingPopover'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; -import { memo } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiArrowsCounterClockwiseBold, @@ -43,6 +44,15 @@ const CurrentImageButtonsContent = memo(({ imageDTO }: { imageDTO: ImageDTO }) = const imageActions = useImageActions(imageDTO); const isStaging = useAppSelector(selectIsStaging); const isUpscalingEnabled = useFeatureStatus('upscaling'); + const { recallWithConfirmation } = useRecallMetadataWithConfirmation(); + + const handleRecallAll = useCallback(() => { + if (imageActions.hasMetadata) { + recallWithConfirmation(() => { + imageActions.recallAll(); + }); + } + }, [imageActions, recallWithConfirmation]); return ( <> @@ -105,15 +115,14 @@ const CurrentImageButtonsContent = memo(({ imageDTO }: { imageDTO: ImageDTO }) = alignSelf="stretch" onClick={imageActions.recallSize} isDisabled={isStaging} - /> - } tooltip={`${t('parameters.useAll')} (A)`} aria-label={`${t('parameters.useAll')} (A)`} isDisabled={!imageActions.hasMetadata} variant="link" alignSelf="stretch" - onClick={imageActions.recallAll} + onClick={handleRecallAll} /> {isUpscalingEnabled && }