diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index 0b448b73428..2d90d21312f 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -58,7 +58,7 @@ "@dnd-kit/sortable": "^8.0.0", "@dnd-kit/utilities": "^3.2.2", "@fontsource-variable/inter": "^5.0.20", - "@invoke-ai/ui-library": "^0.0.34", + "@invoke-ai/ui-library": "^0.0.36", "@nanostores/react": "^0.7.3", "@reduxjs/toolkit": "2.2.3", "@roarr/browser-log-writer": "^1.3.0", diff --git a/invokeai/frontend/web/pnpm-lock.yaml b/invokeai/frontend/web/pnpm-lock.yaml index e6030ac08fa..1c8c30464d9 100644 --- a/invokeai/frontend/web/pnpm-lock.yaml +++ b/invokeai/frontend/web/pnpm-lock.yaml @@ -24,8 +24,8 @@ dependencies: specifier: ^5.0.20 version: 5.0.20 '@invoke-ai/ui-library': - specifier: ^0.0.34 - version: 0.0.34(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1) + specifier: ^0.0.36 + version: 0.0.36(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1) '@nanostores/react': specifier: ^0.7.3 version: 0.7.3(nanostores@0.11.2)(react@18.3.1) @@ -3574,8 +3574,8 @@ packages: prettier: 3.3.3 dev: true - /@invoke-ai/ui-library@0.0.34(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1): - resolution: {integrity: sha512-iDSjFQV2U4LfQ8+UdZ9Uy6J1iKKTSsXM0uhkWrwcIghbgN5QwY3ABVLhqJrSWVTwp7puEDhe/lRQ9QhTZBkVzw==} + /@invoke-ai/ui-library@0.0.36(@chakra-ui/form-control@2.2.0)(@chakra-ui/icon@3.2.0)(@chakra-ui/media-query@3.3.0)(@chakra-ui/menu@2.2.1)(@chakra-ui/spinner@2.1.0)(@chakra-ui/system@2.6.2)(@fontsource-variable/inter@5.0.20)(@types/react@18.3.3)(i18next@23.12.2)(react-dom@18.3.1)(react@18.3.1): + resolution: {integrity: sha512-gF/LSzUYFxDmKiUADvl7lsVEbyOQurWVxeCRD4279lBo/RjVcGDwkflLtP8FN//Wv1CqZqn9UnOrJHpUZWl1Rg==} peerDependencies: '@fontsource-variable/inter': ^5.0.16 react: ^18.2.0 diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index d6183458c46..f932e2f8a71 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -439,7 +439,9 @@ "compareHelp2": "Press M to cycle through comparison modes.", "compareHelp3": "Press C to swap the compared images.", "compareHelp4": "Press Z or Esc to exit.", - "toggleMiniViewer": "Toggle Mini Viewer" + "toggleMiniViewer": "Toggle Mini Viewer", + "openViewer": "Open Viewer", + "closeViewer": "Close Viewer" }, "hotkeys": { "searchHotkeys": "Search Hotkeys", @@ -1723,9 +1725,11 @@ "sendingToCanvas": "Sending to Canvas", "sendingToGallery": "Sending to Gallery", "sendToGallery": "Send To Gallery", - "sendToGalleryDesc": "Generations will be sent to the gallery.", + "sendToGalleryDesc": "Pressing Invoke generates and saves a unique image to your gallery.", "sendToCanvas": "Send To Canvas", - "sendToCanvasDesc": "Generations will be staged onto the canvas.", + "sendToCanvasDesc": "Pressing Invoke stages your work in progress on the canvas.", + "viewProgressInViewer": "View progress and outputs in the Image Viewer.", + "viewProgressOnCanvas": "View progress and stage outputs on the Canvas.", "rasterLayer_withCount_one": "$t(controlLayers.rasterLayer)", "controlLayer_withCount_one": "$t(controlLayers.controlLayer)", "inpaintMask_withCount_one": "$t(controlLayers.inpaintMask)", @@ -2031,6 +2035,7 @@ "events": "Events", "queue": "Queue", "metadata": "Metadata" - } + }, + "showSendingToAlerts": "Alert When Sending to Different View" } } diff --git a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts index 9a40d55841a..634e2ead39c 100644 --- a/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts +++ b/invokeai/frontend/web/src/common/hooks/useGlobalHotkeys.ts @@ -114,13 +114,4 @@ export const useGlobalHotkeys = () => { }, [dispatch, isModelManagerEnabled] ); - - useHotkeys( - isModelManagerEnabled ? '6' : '5', - () => { - dispatch(setActiveTab('gallery')); - setScopes([]); - }, - [dispatch, isModelManagerEnabled] - ); }; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx index 499cbcba7d5..15ecd5a956e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasDropArea.tsx @@ -1,6 +1,7 @@ import { Flex } from '@invoke-ai/ui-library'; import IAIDroppable from 'common/components/IAIDroppable'; import type { AddControlLayerFromImageDropData, AddRasterLayerFromImageDropData } from 'features/dnd/types'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { memo } from 'react'; const addRasterLayerFromImageDropData: AddRasterLayerFromImageDropData = { @@ -14,6 +15,12 @@ const addControlLayerFromImageDropData: AddControlLayerFromImageDropData = { }; export const CanvasDropArea = memo(() => { + const imageViewer = useImageViewer(); + + if (imageViewer.isOpen) { + return null; + } + return ( <> diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx similarity index 89% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx index 87971e31f0e..1a4bf4d2734 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasLayersPanelContent.tsx @@ -7,7 +7,7 @@ import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/Canva import { selectHasEntities } from 'features/controlLayers/store/selectors'; import { memo } from 'react'; -export const CanvasPanelContent = memo(() => { +export const CanvasLayersPanelContent = memo(() => { const hasEntities = useAppSelector(selectHasEntities); return ( @@ -22,4 +22,4 @@ export const CanvasPanelContent = memo(() => { ); }); -CanvasPanelContent.displayName = 'CanvasPanelContent'; +CanvasLayersPanelContent.displayName = 'CanvasLayersPanelContent'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasTabContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx similarity index 88% rename from invokeai/frontend/web/src/features/controlLayers/components/CanvasTabContent.tsx rename to invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx index c228c743e9d..4b2c548885b 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasTabContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasMainPanelContent.tsx @@ -6,6 +6,7 @@ import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea import { Filter } from 'features/controlLayers/components/Filters/Filter'; import { CanvasHUD } from 'features/controlLayers/components/HUD/CanvasHUD'; import { CanvasSelectedEntityStatusAlert } from 'features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert'; +import { SendingToGalleryAlert } from 'features/controlLayers/components/HUD/CanvasSendingToGalleryAlert'; import { InvokeCanvasComponent } from 'features/controlLayers/components/InvokeCanvasComponent'; import { StagingAreaIsStagingGate } from 'features/controlLayers/components/StagingArea/StagingAreaIsStagingGate'; import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar'; @@ -14,9 +15,10 @@ import { Transform } from 'features/controlLayers/components/Transform/Transform import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate'; import { TRANSPARENCY_CHECKERBOARD_PATTERN_DATAURL } from 'features/controlLayers/konva/patterns/transparency-checkerboard-pattern'; import { selectDynamicGrid, selectShowHUD } from 'features/controlLayers/store/canvasSettingsSlice'; +import { GatedImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer'; import { memo, useCallback, useRef } from 'react'; -export const CanvasTabContent = memo(() => { +export const CanvasMainPanelContent = memo(() => { const ref = useRef(null); const dynamicGrid = useAppSelector(selectDynamicGrid); const showHUD = useAppSelector(selectShowHUD); @@ -76,8 +78,9 @@ export const CanvasTabContent = memo(() => { )} - + + @@ -97,8 +100,9 @@ export const CanvasTabContent = memo(() => { + ); }); -CanvasTabContent.displayName = 'CanvasTabContent'; +CanvasMainPanelContent.displayName = 'CanvasMainPanelContent'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx index 40bd0add243..af67dc5320c 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasRightPanel.tsx @@ -1,30 +1,47 @@ import { useDndContext } from '@dnd-kit/core'; -import { Box, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { Box, Button, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs } from '@invoke-ai/ui-library'; +import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { useScopeOnFocus } from 'common/hooks/interactionScopes'; -import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent'; -import { CanvasSendToToggle } from 'features/controlLayers/components/CanvasSendToToggle'; -import { selectSendToCanvas } from 'features/controlLayers/store/canvasSettingsSlice'; +import { CanvasLayersPanelContent } from 'features/controlLayers/components/CanvasLayersPanelContent'; import { selectEntityCountActive } from 'features/controlLayers/store/selectors'; import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent'; -import { memo, useCallback, useMemo, useRef, useState } from 'react'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; +import { atom } from 'nanostores'; +import { memo, useCallback, useMemo, useRef } from 'react'; +import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -export const CanvasRightPanelContent = memo(() => { +const $tabIndex = atom(0); +export const setRightPanelTabToLayers = () => $tabIndex.set(0); +export const setRightPanelTabToGallery = () => $tabIndex.set(1); + +export const CanvasRightPanel = memo(() => { + const { t } = useTranslation(); const ref = useRef(null); - const [tab, setTab] = useState(0); + const tabIndex = useStore($tabIndex); useScopeOnFocus('gallery', ref); + const imageViewer = useImageViewer(); + const onClickViewerToggleButton = useCallback(() => { + if ($tabIndex.get() !== 1) { + $tabIndex.set(1); + } + imageViewer.toggle(); + }, [imageViewer]); + useHotkeys('z', imageViewer.toggle); return ( - + - + - + - + @@ -34,30 +51,29 @@ export const CanvasRightPanelContent = memo(() => { ); }); -CanvasRightPanelContent.displayName = 'CanvasRightPanelContent'; +CanvasRightPanel.displayName = 'CanvasRightPanel'; -const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => { +const PanelTabs = memo(() => { const { t } = useTranslation(); const activeEntityCount = useAppSelector(selectEntityCountActive); - const sendToCanvas = useAppSelector(selectSendToCanvas); const tabTimeout = useRef(null); const dndCtx = useDndContext(); const onOnMouseOverLayersTab = useCallback(() => { tabTimeout.current = window.setTimeout(() => { if (dndCtx.active) { - setTab(0); + setRightPanelTabToLayers(); } }, 300); - }, [dndCtx.active, setTab]); + }, [dndCtx.active]); const onOnMouseOverGalleryTab = useCallback(() => { tabTimeout.current = window.setTimeout(() => { if (dndCtx.active) { - setTab(1); + setRightPanelTabToGallery(); } }, 300); - }, [dndCtx.active, setTab]); + }, [dndCtx.active]); const onMouseOut = useCallback(() => { if (tabTimeout.current) { @@ -78,15 +94,9 @@ const PanelTabs = memo(({ setTab }: { setTab: (val: number) => void }) => { {layersTabLabel} - {sendToCanvas && ( - - )} {t('gallery.gallery')} - {!sendToCanvas && ( - - )} ); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx deleted file mode 100644 index 8e44f44a7f4..00000000000 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasSendToToggle.tsx +++ /dev/null @@ -1,77 +0,0 @@ -import { - Button, - Flex, - Icon, - Popover, - PopoverArrow, - PopoverBody, - PopoverContent, - PopoverTrigger, - Text, -} from '@invoke-ai/ui-library'; -import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { selectSendToCanvas, settingsSendToCanvasChanged } from 'features/controlLayers/store/canvasSettingsSlice'; -import { memo, useCallback } from 'react'; -import { useTranslation } from 'react-i18next'; -import { PiCaretDownBold, PiCheckBold } from 'react-icons/pi'; - -export const CanvasSendToToggle = memo(() => { - const { t } = useTranslation(); - const sendToCanvas = useAppSelector(selectSendToCanvas); - const dispatch = useAppDispatch(); - - const enableSendToCanvas = useCallback(() => { - dispatch(settingsSendToCanvasChanged(true)); - }, [dispatch]); - - const disableSendToCanvas = useCallback(() => { - dispatch(settingsSendToCanvasChanged(false)); - }, [dispatch]); - - return ( - - - - - - - - - - - - - - - ); -}); - -CanvasSendToToggle.displayName = 'CanvasSendToToggle'; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert.tsx index 9042ca3dce8..d510556f898 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/HUD/CanvasSelectedEntityStatusAlert.tsx @@ -1,8 +1,8 @@ -import { Box, Flex, Icon, Text } from '@invoke-ai/ui-library'; +import type { AlertStatus } from '@invoke-ai/ui-library'; +import { Alert, AlertDescription, AlertIcon, AlertTitle } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import type { Property } from 'csstype'; import { useEntityAdapterSafe } from 'features/controlLayers/contexts/EntityAdapterContext'; import { useEntityTitle } from 'features/controlLayers/hooks/useEntityTitle'; import { useEntityTypeIsHidden } from 'features/controlLayers/hooks/useEntityTypeIsHidden'; @@ -16,7 +16,6 @@ import type { CanvasEntityIdentifier } from 'features/controlLayers/store/types' import { atom } from 'nanostores'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiWarningCircleFill } from 'react-icons/pi'; type ContentProps = { entityIdentifier: CanvasEntityIdentifier; @@ -25,9 +24,10 @@ type ContentProps = { const $isFilteringFallback = atom(false); -type EntityStatus = { - value: string; - color?: Property.Color; +type AlertData = { + status: AlertStatus; + title: string; + description: string; }; const CanvasSelectedEntityStatusAlertContent = memo(({ entityIdentifier, adapter }: ContentProps) => { @@ -47,73 +47,60 @@ const CanvasSelectedEntityStatusAlertContent = memo(({ entityIdentifier, adapter const isFiltering = useStore(adapter.filterer?.$isFiltering ?? $isFilteringFallback); const isTransforming = useStore(adapter.transformer.$isTransforming); - const status = useMemo(() => { + const alert = useMemo(() => { if (isFiltering) { return { - value: t('controlLayers.HUD.entityStatus.isFiltering'), - color: 'invokeBlue.300', + status: 'info', + title, + description: t('controlLayers.HUD.entityStatus.isFiltering'), }; } if (isTransforming) { return { - value: t('controlLayers.HUD.entityStatus.isTransforming'), - color: 'invokeBlue.300', + status: 'info', + title, + description: t('controlLayers.HUD.entityStatus.isTransforming'), }; } if (isHidden) { return { - value: t('controlLayers.HUD.entityStatus.isHidden'), - color: 'invokePurple.300', + status: 'warning', + title, + description: t('controlLayers.HUD.entityStatus.isHidden'), }; } if (isLocked) { return { - value: t('controlLayers.HUD.entityStatus.isLocked'), - color: 'invokeRed.300', + status: 'warning', + title, + description: t('controlLayers.HUD.entityStatus.isLocked'), }; } if (!isEnabled) { return { - value: t('controlLayers.HUD.entityStatus.isDisabled'), - color: 'invokeRed.300', + status: 'warning', + title, + description: t('controlLayers.HUD.entityStatus.isDisabled'), }; } return null; - }, [isFiltering, isTransforming, isHidden, isLocked, isEnabled, t]); + }, [isFiltering, isTransforming, isHidden, isLocked, isEnabled, title, t]); - if (!status) { + if (!alert) { return null; } return ( - - - - - - - {title} - {' '} - {status.value} - - - + + + {alert.title} + {alert.description}. + ); }); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/HUD/CanvasSendingToGalleryAlert.tsx b/invokeai/frontend/web/src/features/controlLayers/components/HUD/CanvasSendingToGalleryAlert.tsx new file mode 100644 index 00000000000..d38b60c49eb --- /dev/null +++ b/invokeai/frontend/web/src/features/controlLayers/components/HUD/CanvasSendingToGalleryAlert.tsx @@ -0,0 +1,161 @@ +import { + Alert, + AlertDescription, + AlertIcon, + AlertTitle, + Button, + Flex, + Icon, + IconButton, + Spacer, +} from '@invoke-ai/ui-library'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { useBoolean } from 'common/hooks/useBoolean'; +import { + setRightPanelTabToGallery, + setRightPanelTabToLayers, +} from 'features/controlLayers/components/CanvasRightPanel'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; +import { useCurrentDestination } from 'features/queue/hooks/useCurrentDestination'; +import { selectShowSendingToAlerts, showSendingToAlertsChanged } from 'features/system/store/systemSlice'; +import { setActiveTab } from 'features/ui/store/uiSlice'; +import { AnimatePresence, motion } from 'framer-motion'; +import type { PropsWithChildren, ReactNode } from 'react'; +import { useCallback, useMemo } from 'react'; +import { Trans, useTranslation } from 'react-i18next'; +import { PiXBold } from 'react-icons/pi'; + +const ActivateImageViewerButton = (props: PropsWithChildren) => { + const imageViewer = useImageViewer(); + const onClick = useCallback(() => { + imageViewer.open(); + setRightPanelTabToGallery(); + }, [imageViewer]); + return ( + + ); +}; + +export const SendingToGalleryAlert = () => { + const { t } = useTranslation(); + const destination = useCurrentDestination(); + const isVisible = useMemo(() => { + if (!destination) { + return false; + } + + if (destination === 'canvas') { + return false; + } + + return true; + }, [destination]); + + return ( + }} /> + } + isVisible={isVisible} + /> + ); +}; + +const ActivateCanvasButton = (props: PropsWithChildren) => { + const dispatch = useAppDispatch(); + const imageViewer = useImageViewer(); + const onClick = useCallback(() => { + dispatch(setActiveTab('generation')); + setRightPanelTabToLayers(); + imageViewer.close(); + }, [dispatch, imageViewer]); + return ( + + ); +}; + +export const SendingToCanvasAlert = () => { + const { t } = useTranslation(); + const destination = useCurrentDestination(); + const isVisible = useMemo(() => { + if (!destination) { + return false; + } + + if (destination !== 'canvas') { + return false; + } + + return true; + }, [destination]); + + return ( + }} /> + } + isVisible={isVisible} + /> + ); +}; + +const AlertWrapper = ({ + title, + description, + isVisible, +}: { + title: ReactNode; + description: ReactNode; + isVisible: boolean; +}) => { + const { t } = useTranslation(); + const dispatch = useAppDispatch(); + const showSendToAlerts = useAppSelector(selectShowSendingToAlerts); + const isHovered = useBoolean(false); + const onClickDontShowMeThese = useCallback(() => { + dispatch(showSendingToAlertsChanged(false)); + isHovered.setFalse(); + }, [dispatch, isHovered]); + + return ( + + {(isVisible || isHovered.isTrue) && showSendToAlerts && ( + + + + + {title} + + } + tooltip={t('common.dontShowMeThese')} + aria-label={t('common.dontShowMeThese')} + right={-1} + top={-2} + onClick={onClickDontShowMeThese} + minW="auto" + /> + + {description} + + + )} + + ); +}; diff --git a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx index fdbe7de50fc..0ef57047ed4 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Gallery.tsx @@ -14,14 +14,12 @@ import { import { createSelector } from '@reduxjs/toolkit'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm'; -import CurrentImageButtons from 'features/gallery/components/ImageViewer/CurrentImageButtons'; -import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview'; -import { selectIsMiniViewerOpen, selectSelectedBoardId } from 'features/gallery/store/gallerySelectors'; -import { galleryViewChanged, isMiniViewerOpenToggled, selectGallerySlice } from 'features/gallery/store/gallerySlice'; +import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors'; +import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice'; import type { CSSProperties } from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiEyeBold, PiEyeClosedBold, PiMagnifyingGlassBold } from 'react-icons/pi'; +import { PiMagnifyingGlassBold } from 'react-icons/pi'; import { useBoardName } from 'services/api/hooks/useBoardName'; import GalleryImageGrid from './ImageGrid/GalleryImageGrid'; @@ -52,11 +50,6 @@ export const Gallery = () => { const initialSearchTerm = useAppSelector(selectSearchTerm); const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 }); const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm(); - const isMiniViewerOpen = useAppSelector(selectIsMiniViewerOpen); - - const toggleMiniViewer = useCallback(() => { - dispatch(isMiniViewerOpenToggled()); - }, [dispatch]); const handleClickImages = useCallback(() => { dispatch(galleryViewChanged('images')); }, [dispatch]); @@ -87,27 +80,15 @@ export const Gallery = () => { {t('gallery.assets')} - - : } - colorScheme={isMiniViewerOpen ? 'invokeBlue' : 'base'} - /> - } - /> - + } + /> @@ -120,21 +101,6 @@ export const Gallery = () => { /> - - - - - - - - diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx index 6f4962a3c7f..0ce467d4a47 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageContextMenu/ImageMenuItemOpenInViewer.tsx @@ -1,25 +1,25 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppDispatch } from 'app/store/storeHooks'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { useImageDTOContext } from 'features/gallery/contexts/ImageDTOContext'; import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice'; -import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; -import { PiEyeBold } from 'react-icons/pi'; +import { PiArrowsOutBold } from 'react-icons/pi'; export const ImageMenuItemOpenInViewer = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const imageDTO = useImageDTOContext(); - + const imageViewer = useImageViewer(); const onClick = useCallback(() => { dispatch(imageToCompareChanged(null)); dispatch(imageSelected(imageDTO)); - dispatch(setActiveTab('gallery')); - }, [dispatch, imageDTO]); + imageViewer.open(); + }, [dispatch, imageDTO, imageViewer]); return ( - } onClick={onClick}> + } onClick={onClick}> {t('gallery.openInViewer')} ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx index 7dca9ecac2f..87915e4928b 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GalleryImage.tsx @@ -10,11 +10,11 @@ import IAIFillSkeleton from 'common/components/IAIFillSkeleton'; import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice'; import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggableData } from 'features/dnd/types'; import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId'; +import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer'; import { useMultiselect } from 'features/gallery/hooks/useMultiselect'; import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView'; import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors'; import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice'; -import { setActiveTab } from 'features/ui/store/uiSlice'; import type { MouseEvent, MouseEventHandler } from 'react'; import { memo, useCallback, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -112,10 +112,11 @@ const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => { setIsHovered(true); }, []); + const imageViewer = useImageViewer(); const onDoubleClick = useCallback(() => { - dispatch(setActiveTab('gallery')); + imageViewer.open(); dispatch(imageToCompareChanged(null)); - }, [dispatch]); + }, [dispatch, imageViewer]); const handleMouseOut = useCallback(() => { setIsHovered(false); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx index bd7faf86e03..cc6872ab7eb 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGrid/GallerySelectionCountTag.tsx @@ -22,8 +22,11 @@ export const GallerySelectionCountTag = () => { const isSelectAllEnabled = useStore($isSelectAllEnabled); const onClearSelection = useCallback(() => { - dispatch(selectionChanged([])); - }, [dispatch]); + const firstImage = selection[0]; + if (firstImage) { + dispatch(selectionChanged([firstImage])); + } + }, [dispatch, selection]); const onSelectPage = useCallback(() => { dispatch(selectionChanged([...selection, ...imageDTOs])); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx index 637d282e171..e2f2944c314 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CompareToolbar.tsx @@ -21,7 +21,7 @@ import { import { memo, useCallback } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; import { Trans, useTranslation } from 'react-i18next'; -import { PiArrowsOutBold, PiQuestion, PiSwapBold, PiXBold } from 'react-icons/pi'; +import { PiArrowsOutBold, PiQuestion, PiSwapBold } from 'react-icons/pi'; export const CompareToolbar = memo(() => { const { t } = useTranslation(); @@ -104,15 +104,17 @@ export const CompareToolbar = memo(() => { }> - + - } + diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx index 193ebec361a..60c4db26e86 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/CurrentImagePreview.tsx @@ -15,7 +15,7 @@ import { memo, useCallback, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { PiImageBold } from 'react-icons/pi'; import { useGetImageDTOQuery } from 'services/api/endpoints/images'; -import { $hasProgress } from 'services/events/setEventListeners'; +import { $hasProgress, $isProgressFromCanvas } from 'services/events/setEventListeners'; import ProgressImage from './ProgressImage'; @@ -24,6 +24,7 @@ const CurrentImagePreview = () => { const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails); const imageName = useAppSelector(selectLastSelectedImageName); const hasDenoiseProgress = useStore($hasProgress); + const isProgressFromCanvas = useStore($isProgressFromCanvas); const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken); @@ -61,7 +62,7 @@ const CurrentImagePreview = () => { justifyContent="center" position="relative" > - {hasDenoiseProgress && shouldShowProgressInViewer ? ( + {hasDenoiseProgress && !isProgressFromCanvas && shouldShowProgressInViewer ? ( ) : ( { +import { useImageViewer } from './useImageViewer'; + +type Props = { + closeButton?: ReactNode; +}; + +export const ImageViewer = memo(({ closeButton }: Props) => { + useAssertSingleton('ImageViewer'); const hasImageToCompare = useAppSelector(selectHasImageToCompare); const [containerRef, containerDims] = useMeasure(); const ref = useRef(null); useScopeOnFocus('imageViewer', ref); useScopeOnMount('imageViewer'); + useEffect(() => { + ref?.current?.focus(); + }, []); + return ( { justifyContent="center" > {hasImageToCompare && } - {!hasImageToCompare && } + {!hasImageToCompare && } {!hasImageToCompare && } {hasImageToCompare && } + + + ); }); ImageViewer.displayName = 'ImageViewer'; + +export const GatedImageViewer = memo(() => { + const imageViewer = useImageViewer(); + + if (!imageViewer.isOpen) { + return null; + } + + return } />; +}); + +GatedImageViewer.displayName = 'GatedImageViewer'; + +const ImageViewerCloseButton = memo(() => { + const { t } = useTranslation(); + const imageViewer = useImageViewer(); + useAssertSingleton('ImageViewerCloseButton'); + useHotkeys('esc', imageViewer.close); + return ( + + ); +}); + +ImageViewerCloseButton.displayName = 'ImageViewerCloseButton'; + +const GatedImageViewerCloseButton = memo(() => { + const imageViewer = useImageViewer(); + + if (!imageViewer.isOpen) { + return null; + } + + return ; +}); + +GatedImageViewerCloseButton.displayName = 'GatedImageViewerCloseButton'; diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx index 6626592294d..2b8013f22ea 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ProgressImage.tsx @@ -5,7 +5,7 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { selectSystemSlice } from 'features/system/store/systemSlice'; import { memo, useMemo } from 'react'; -import { $progressImage } from 'services/events/setEventListeners'; +import { $isProgressFromCanvas, $progressImage } from 'services/events/setEventListeners'; const selectShouldAntialiasProgressImage = createSelector( selectSystemSlice, @@ -14,6 +14,7 @@ const selectShouldAntialiasProgressImage = createSelector( const CurrentImagePreview = () => { const progressImage = useStore($progressImage); + const isProgressFromCanvas = useStore($isProgressFromCanvas); const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage); const sx = useMemo( @@ -23,7 +24,7 @@ const CurrentImagePreview = () => { [shouldAntialiasProgressImage] ); - if (!progressImage) { + if (!progressImage || isProgressFromCanvas) { return null; } diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx index 5a66e34039a..690087c6d0e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/ViewerToolbar.tsx @@ -1,11 +1,16 @@ import { Flex } from '@invoke-ai/ui-library'; import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton'; import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton'; +import type { ReactNode } from 'react'; import { memo } from 'react'; import CurrentImageButtons from './CurrentImageButtons'; -export const ViewerToolbar = memo(() => { +type Props = { + closeButton?: ReactNode; +}; + +export const ViewerToolbar = memo(({ closeButton }: Props) => { return ( @@ -18,7 +23,9 @@ export const ViewerToolbar = memo(() => { - + + {closeButton} + ); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts new file mode 100644 index 00000000000..732a156e978 --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageViewer/useImageViewer.ts @@ -0,0 +1,14 @@ +import { buildUseBoolean } from 'common/hooks/useBoolean'; +import { useMemo } from 'react'; + +const [useImageViewerState, $imageViewerState] = buildUseBoolean(true); + +export const useImageViewer = () => { + const imageViewerState = useImageViewerState(); + const isOpen = useMemo(() => imageViewerState.isTrue, [imageViewerState]); + const open = useMemo(() => imageViewerState.setTrue, [imageViewerState]); + const close = useMemo(() => imageViewerState.setFalse, [imageViewerState]); + const toggle = useMemo(() => imageViewerState.toggle, [imageViewerState]); + + return { isOpen, open, close, toggle, $state: $imageViewerState }; +}; diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 8f1e8668754..c7b4daa92c9 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -57,4 +57,3 @@ export const selectImageToCompare = createSelector(selectGallerySlice, (gallery) export const selectHasImageToCompare = createSelector(selectImageToCompare, (imageToCompare) => Boolean(imageToCompare) ); -export const selectIsMiniViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isMiniViewerOpen); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 69e8d6ac4c2..a9f380d7eb7 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -25,7 +25,6 @@ const initialGalleryState: GalleryState = { comparisonMode: 'slider', comparisonFit: 'fill', shouldShowArchivedBoards: false, - isMiniViewerOpen: false, }; export const gallerySlice = createSlice({ @@ -88,9 +87,6 @@ export const gallerySlice = createSlice({ alwaysShowImageSizeBadgeChanged: (state, action: PayloadAction) => { state.alwaysShowImageSizeBadge = action.payload; }, - isMiniViewerOpenToggled: (state) => { - state.isMiniViewerOpen = !state.isMiniViewerOpen; - }, comparedImagesSwapped: (state) => { if (state.imageToCompare) { const oldSelection = state.selection; @@ -146,7 +142,6 @@ export const { starredFirstChanged, shouldShowArchivedBoardsChanged, searchTermChanged, - isMiniViewerOpenToggled, } = gallerySlice.actions; export const selectGallerySlice = (state: RootState) => state.gallery; diff --git a/invokeai/frontend/web/src/features/gallery/store/types.ts b/invokeai/frontend/web/src/features/gallery/store/types.ts index 4ded8c494e4..48bbc8b7be1 100644 --- a/invokeai/frontend/web/src/features/gallery/store/types.ts +++ b/invokeai/frontend/web/src/features/gallery/store/types.ts @@ -28,5 +28,4 @@ export type GalleryState = { comparisonMode: ComparisonMode; comparisonFit: ComparisonFit; shouldShowArchivedBoards: boolean; - isMiniViewerOpen: boolean; }; diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index 1d58c878cf1..5b29f4b6ee9 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -15,7 +15,7 @@ export const InvokeQueueBackButton = memo(() => { const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); return ( - +