diff --git a/invokeai/frontend/web/public/locales/en.json b/invokeai/frontend/web/public/locales/en.json index e5827c2397..e3f21e50a1 100644 --- a/invokeai/frontend/web/public/locales/en.json +++ b/invokeai/frontend/web/public/locales/en.json @@ -531,7 +531,7 @@ "useCanvasBeta": "Use Canvas Beta Layout", "enableImageDebugging": "Enable Image Debugging", "useSlidersForAll": "Use Sliders For All Options", - "autoShowProgress": "Auto Show Progress Images", + "showProgressInViewer": "Show Progress Images in Viewer", "resetWebUI": "Reset Web UI", "resetWebUIDesc1": "Resetting the web UI only resets the browser's local cache of your images and remembered settings. It does not delete any images from disk.", "resetWebUIDesc2": "If images aren't showing up in the gallery or something else isn't working, please try resetting before submitting an issue on GitHub.", diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index f65947a1fa..b49e44e554 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -27,7 +27,7 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys'; import { configChanged } from 'features/system/store/configSlice'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; import { useLogger } from 'app/logging/useLogger'; -import ProgressImagePreview from 'features/parameters/components/ProgressImagePreview'; +import ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview'; import ParametersDrawer from 'features/ui/components/ParametersDrawer'; const DEFAULT_CONFIG = {}; @@ -124,7 +124,6 @@ const App = ({ config = DEFAULT_CONFIG, children }: Props) => { - ); }; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts index c9d8e45886..24b85e0f83 100644 --- a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -10,6 +10,7 @@ export const actionSanitizer = (action: A): A => { // Sanitize nodes as needed forEach(action.payload.nodes, (node, key) => { + // Don't log the whole freaking dataURL if (node.type === 'dataURL_image') { const { dataURL, ...rest } = node; sanitizedNodes[key] = { ...rest, dataURL: '' }; diff --git a/invokeai/frontend/web/src/common/components/IAIPopover.tsx b/invokeai/frontend/web/src/common/components/IAIPopover.tsx index ba3fbdd109..51562b969c 100644 --- a/invokeai/frontend/web/src/common/components/IAIPopover.tsx +++ b/invokeai/frontend/web/src/common/components/IAIPopover.tsx @@ -27,7 +27,7 @@ const IAIPopover = (props: IAIPopoverProps) => { return ( {triggerComponent} - + {hasArrow && } {children} diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx index 32e631005d..2a3d12ce91 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImageButtons.tsx @@ -5,7 +5,13 @@ import { ButtonGroup, Flex, FlexProps, + IconButton, Link, + Menu, + MenuButton, + MenuItemOption, + MenuList, + MenuOptionGroup, useDisclosure, useToast, } from '@chakra-ui/react'; @@ -46,6 +52,7 @@ import { FaShare, FaShareAlt, FaTrash, + FaWrench, } from 'react-icons/fa'; import { gallerySelector, @@ -62,6 +69,7 @@ import { requestedImageDeletion } from '../store/actions'; import FaceRestoreSettings from 'features/parameters/components/Parameters/FaceRestore/FaceRestoreSettings'; import UpscaleSettings from 'features/parameters/components/Parameters/Upscale/UpscaleSettings'; import { allParametersSet } from 'features/parameters/store/generationSlice'; +import DeleteImageButton from './ImageActionButtons/DeleteImageButton'; const currentImageButtonsSelector = createSelector( [ @@ -451,7 +459,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { - : } tooltip={ !shouldHidePreview @@ -465,7 +473,7 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { } isChecked={shouldHidePreview} onClick={handlePreviewVisibility} - /> + /> */} {isLightboxEnabled && ( } @@ -592,23 +600,9 @@ const CurrentImageButtons = (props: CurrentImageButtonsProps) => { - } - tooltip={`${t('gallery.deleteImage')} (Del)`} - aria-label={`${t('gallery.deleteImage')} (Del)`} - isDisabled={!image || !isConnected} - colorScheme="error" - /> + - {image && ( - - )} ); }; diff --git a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx index 1f18fb9b9a..c5bcee540a 100644 --- a/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/CurrentImagePreview.tsx @@ -1,4 +1,4 @@ -import { Box, Flex, Image } from '@chakra-ui/react'; +import { Box, Flex, Image, Skeleton, useBoolean } from '@chakra-ui/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useGetUrl } from 'common/util/getUrl'; @@ -10,16 +10,25 @@ import ImageMetadataViewer from './ImageMetaDataViewer/ImageMetadataViewer'; import NextPrevImageButtons from './NextPrevImageButtons'; import CurrentImageHidden from './CurrentImageHidden'; import { DragEvent, memo, useCallback } from 'react'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import CurrentImageFallback from './CurrentImageFallback'; export const imagesSelector = createSelector( - [uiSelector, gallerySelector], - (ui, gallery) => { - const { shouldShowImageDetails, shouldHidePreview } = ui; + [uiSelector, gallerySelector, systemSelector], + (ui, gallery, system) => { + const { + shouldShowImageDetails, + shouldHidePreview, + shouldShowProgressInViewer, + } = ui; const { selectedImage } = gallery; + const { progressImage } = system; return { shouldShowImageDetails, shouldHidePreview, image: selectedImage, + progressImage, + shouldShowProgressInViewer, }; }, { @@ -30,10 +39,17 @@ export const imagesSelector = createSelector( ); const CurrentImagePreview = () => { - const { shouldShowImageDetails, image, shouldHidePreview } = - useAppSelector(imagesSelector); + const { + shouldShowImageDetails, + image, + shouldHidePreview, + progressImage, + shouldShowProgressInViewer, + } = useAppSelector(imagesSelector); const { getUrl } = useGetUrl(); + const [isLoaded, { on, off }] = useBoolean(); + const handleDragStart = useCallback( (e: DragEvent) => { if (!image) { @@ -56,13 +72,11 @@ const CurrentImagePreview = () => { height: '100%', }} > - {image && ( + {progressImage && shouldShowProgressInViewer ? ( : undefined} + src={progressImage.dataURL} + width={progressImage.width} + height={progressImage.height} sx={{ objectFit: 'contain', maxWidth: '100%', @@ -72,6 +86,31 @@ const CurrentImagePreview = () => { borderRadius: 'base', }} /> + ) : ( + image && ( + + ) : ( + + ) + } + sx={{ + objectFit: 'contain', + maxWidth: '100%', + maxHeight: '100%', + height: 'auto', + position: 'absolute', + borderRadius: 'base', + }} + /> + ) )} {shouldShowImageDetails && image && 'metadata' in image && ( { + const { shouldUseSingleGalleryColumn, galleryImageObjectFit } = gallery; + const { progressImage } = system; + + return { + progressImage, + shouldUseSingleGalleryColumn, + galleryImageObjectFit, + }; + }, + defaultSelectorOptions +); + +const GalleryProgressImage = () => { + const { progressImage, shouldUseSingleGalleryColumn, galleryImageObjectFit } = + useAppSelector(selector); + + if (!progressImage) { + return null; + } + + return ( + + + + ); +}; + +export default memo(GalleryProgressImage); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx new file mode 100644 index 0000000000..6e35ccd63b --- /dev/null +++ b/invokeai/frontend/web/src/features/gallery/components/ImageActionButtons/DeleteImageButton.tsx @@ -0,0 +1,92 @@ +import { createSelector } from '@reduxjs/toolkit'; + +import { useDisclosure } from '@chakra-ui/react'; +import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import IAIIconButton from 'common/components/IAIIconButton'; +import { systemSelector } from 'features/system/store/systemSelectors'; + +import { useHotkeys } from 'react-hotkeys-hook'; +import { useTranslation } from 'react-i18next'; +import { FaTrash } from 'react-icons/fa'; +import { memo, useCallback } from 'react'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import DeleteImageModal from '../DeleteImageModal'; +import { requestedImageDeletion } from 'features/gallery/store/actions'; +import { Image } from 'app/types/invokeai'; + +const selector = createSelector( + [systemSelector], + (system) => { + const { isProcessing, isConnected, shouldConfirmOnDelete } = system; + + return { + canDeleteImage: isConnected && !isProcessing, + shouldConfirmOnDelete, + isProcessing, + isConnected, + }; + }, + defaultSelectorOptions +); + +type DeleteImageButtonProps = { + image: Image | undefined; +}; + +const DeleteImageButton = (props: DeleteImageButtonProps) => { + const { image } = props; + const dispatch = useAppDispatch(); + const { isProcessing, isConnected, canDeleteImage, shouldConfirmOnDelete } = + useAppSelector(selector); + + const { + isOpen: isDeleteDialogOpen, + onOpen: onDeleteDialogOpen, + onClose: onDeleteDialogClose, + } = useDisclosure(); + + const { t } = useTranslation(); + + const handleDelete = useCallback(() => { + if (canDeleteImage && image) { + dispatch(requestedImageDeletion(image)); + } + }, [image, canDeleteImage, dispatch]); + + const handleInitiateDelete = useCallback(() => { + if (shouldConfirmOnDelete) { + onDeleteDialogOpen(); + } else { + handleDelete(); + } + }, [shouldConfirmOnDelete, onDeleteDialogOpen, handleDelete]); + + useHotkeys('delete', handleInitiateDelete, [ + image, + shouldConfirmOnDelete, + isConnected, + isProcessing, + ]); + + return ( + <> + } + tooltip={`${t('gallery.deleteImage')} (Del)`} + aria-label={`${t('gallery.deleteImage')} (Del)`} + isDisabled={!image || !isConnected} + colorScheme="error" + /> + {image && ( + + )} + + ); +}; + +export default memo(DeleteImageButton); diff --git a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx index 19e9c7c213..1426aff43d 100644 --- a/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/ImageGalleryContent.tsx @@ -5,6 +5,7 @@ import { FlexProps, Grid, Icon, + Image, Text, forwardRef, } from '@chakra-ui/react'; @@ -14,7 +15,10 @@ import IAICheckbox from 'common/components/IAICheckbox'; import IAIIconButton from 'common/components/IAIIconButton'; import IAIPopover from 'common/components/IAIPopover'; import IAISlider from 'common/components/IAISlider'; -import { imageGallerySelector } from 'features/gallery/store/gallerySelectors'; +import { + gallerySelector, + imageGallerySelector, +} from 'features/gallery/store/gallerySelectors'; import { setCurrentCategory, setGalleryImageMinimumWidth, @@ -50,30 +54,48 @@ import { uploadsAdapter } from '../store/uploadsSlice'; import { createSelector } from '@reduxjs/toolkit'; import { RootState } from 'app/store/store'; import { Virtuoso, VirtuosoGrid } from 'react-virtuoso'; +import ProgressImagePreview from 'features/parameters/components/_ProgressImagePreview'; +import ProgressImage from 'features/parameters/components/ProgressImage'; +import { systemSelector } from 'features/system/store/systemSelectors'; +import { Image as ImageType } from 'app/types/invokeai'; +import { ProgressImage as ProgressImageType } from 'services/events/types'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; +import GalleryProgressImage from './GalleryProgressImage'; const GALLERY_SHOW_BUTTONS_MIN_WIDTH = 290; +const PROGRESS_IMAGE_PLACEHOLDER = 'PROGRESS_IMAGE_PLACEHOLDER'; -const gallerySelector = createSelector( - [ - (state: RootState) => state.uploads, - (state: RootState) => state.results, - (state: RootState) => state.gallery, - ], - (uploads, results, gallery) => { +const selector = createSelector( + [(state: RootState) => state], + (state) => { + const { results, uploads, system, gallery } = state; const { currentCategory } = gallery; - return currentCategory === 'results' - ? { - images: resultsAdapter.getSelectors().selectAll(results), - isLoading: results.isLoading, - areMoreImagesAvailable: results.page < results.pages - 1, - } - : { - images: uploadsAdapter.getSelectors().selectAll(uploads), - isLoading: uploads.isLoading, - areMoreImagesAvailable: uploads.page < uploads.pages - 1, - }; - } + const tempImages: (ImageType | typeof PROGRESS_IMAGE_PLACEHOLDER)[] = []; + + if (system.progressImage) { + tempImages.push(PROGRESS_IMAGE_PLACEHOLDER); + } + + if (currentCategory === 'results') { + return { + images: tempImages.concat( + resultsAdapter.getSelectors().selectAll(results) + ), + isLoading: results.isLoading, + areMoreImagesAvailable: results.page < results.pages - 1, + }; + } + + return { + images: tempImages.concat( + uploadsAdapter.getSelectors().selectAll(uploads) + ), + isLoading: uploads.isLoading, + areMoreImagesAvailable: uploads.page < uploads.pages - 1, + }; + }, + defaultSelectorOptions ); const ImageGalleryContent = () => { @@ -108,7 +130,7 @@ const ImageGalleryContent = () => { } = useAppSelector(imageGallerySelector); const { images, areMoreImagesAvailable, isLoading } = - useAppSelector(gallerySelector); + useAppSelector(selector); const handleClickLoadMore = () => { if (currentCategory === 'results') { @@ -186,8 +208,6 @@ const ImageGalleryContent = () => { h: 'full', w: 'full', borderRadius: 'base', - // bg: 'base.850', - // p: 2, }} > { endReached={handleEndReached} scrollerRef={(ref) => setScrollerRef(ref)} itemContent={(index, image) => { - const { name } = image; - const isSelected = selectedImage?.name === name; + const isSelected = + image === PROGRESS_IMAGE_PLACEHOLDER + ? false + : selectedImage?.name === image?.name; return ( - + {image === PROGRESS_IMAGE_PLACEHOLDER ? ( + + ) : ( + + )} ); }} @@ -336,12 +364,16 @@ const ImageGalleryContent = () => { }} scrollerRef={setScroller} itemContent={(index, image) => { - const { name } = image; - const isSelected = selectedImage?.name === name; + const isSelected = + image === PROGRESS_IMAGE_PLACEHOLDER + ? false + : selectedImage?.name === image?.name; - return ( + return image === PROGRESS_IMAGE_PLACEHOLDER ? ( + + ) : ( diff --git a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts index df5b65e296..2d7b88a9ab 100644 --- a/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts +++ b/invokeai/frontend/web/src/features/nodes/util/nodeBuilders/buildImageToImageNode.ts @@ -28,13 +28,14 @@ export const buildImg2ImgNode = ( img2imgStrength: strength, shouldFitToWidthHeight: fit, shouldRandomizeSeed, + initialImage, } = generation; - const initialImage = initialImageSelector(state); + // const initialImage = initialImageSelector(state); if (!initialImage) { // TODO: handle this - // throw 'no initial image'; + throw 'no initial image'; } const imageToImageNode: ImageToImageInvocation = { @@ -47,12 +48,10 @@ export const buildImg2ImgNode = ( cfg_scale: cfgScale, scheduler: sampler as ImageToImageInvocation['scheduler'], model, - image: initialImage - ? { - image_name: initialImage.name, - image_type: initialImage.type, - } - : undefined, + image: { + image_name: initialImage.name, + image_type: initialImage.type, + }, strength, fit, }; diff --git a/invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx b/invokeai/frontend/web/src/features/parameters/components/_ProgressImagePreview.tsx similarity index 100% rename from invokeai/frontend/web/src/features/parameters/components/ProgressImagePreview.tsx rename to invokeai/frontend/web/src/features/parameters/components/_ProgressImagePreview.tsx diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index bf4004a79d..0557228020 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -28,7 +28,7 @@ import { } from 'features/system/store/systemSlice'; import { uiSelector } from 'features/ui/store/uiSelectors'; import { - setShouldAutoShowProgressImages, + setShouldShowProgressInViewer, setShouldUseCanvasBetaLayout, setShouldUseSliders, } from 'features/ui/store/uiSlice'; @@ -54,7 +54,7 @@ const selector = createSelector( const { shouldUseCanvasBetaLayout, shouldUseSliders, - shouldAutoShowProgressImages, + shouldShowProgressInViewer, } = ui; return { @@ -63,7 +63,7 @@ const selector = createSelector( enableImageDebugging, shouldUseCanvasBetaLayout, shouldUseSliders, - shouldAutoShowProgressImages, + shouldShowProgressInViewer, consoleLogLevel, shouldLogToConsole, }; @@ -114,7 +114,7 @@ const SettingsModal = ({ children }: SettingsModalProps) => { enableImageDebugging, shouldUseCanvasBetaLayout, shouldUseSliders, - shouldAutoShowProgressImages, + shouldShowProgressInViewer, consoleLogLevel, shouldLogToConsole, } = useAppSelector(selector); @@ -197,10 +197,10 @@ const SettingsModal = ({ children }: SettingsModalProps) => { } /> ) => - dispatch(setShouldAutoShowProgressImages(e.target.checked)) + dispatch(setShouldShowProgressInViewer(e.target.checked)) } /> diff --git a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx index 18d71d2e41..cd0a4eacc3 100644 --- a/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx +++ b/invokeai/frontend/web/src/features/system/components/StatusIndicator.tsx @@ -9,6 +9,7 @@ import { AnimatePresence, motion } from 'framer-motion'; import { useMemo, useRef } from 'react'; import { FaCircle } from 'react-icons/fa'; import { useHoverDirty } from 'react-use'; +import { defaultSelectorOptions } from 'app/store/util/defaultMemoizeOptions'; const statusIndicatorSelector = createSelector( systemSelector, @@ -31,9 +32,7 @@ const statusIndicatorSelector = createSelector( currentStatusHasSteps, }; }, - { - memoizeOptions: { resultEqualityCheck: isEqual }, - } + defaultSelectorOptions ); const StatusIndicator = () => { diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 6ead700923..8e0ab1f3aa 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -354,6 +354,7 @@ export const systemSlice = createSlice({ state.currentStep = 0; state.totalSteps = 0; state.statusTranslationKey = 'common.statusProcessingComplete'; + state.progressImage = null; if (state.canceledSession === data.graph_execution_state_id) { state.isProcessing = false; @@ -373,6 +374,7 @@ export const systemSlice = createSlice({ state.currentStep = 0; state.totalSteps = 0; state.statusTranslationKey = 'common.statusError'; + state.progressImage = null; state.toastQueue.push( makeToast({ title: t('toast.serverError'), status: 'error' }) diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 60f65079cf..470ad076a3 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -23,7 +23,7 @@ export const initialUIState: UIState = { canvasTabAccordionState: [], floatingProgressImageRect: { x: 0, y: 0, width: 0, height: 0 }, shouldShowProgressImages: false, - shouldAutoShowProgressImages: false, + shouldShowProgressInViewer: false, shouldShowImageParameters: false, }; @@ -135,11 +135,8 @@ export const uiSlice = createSlice({ setShouldShowProgressImages: (state, action: PayloadAction) => { state.shouldShowProgressImages = action.payload; }, - setShouldAutoShowProgressImages: ( - state, - action: PayloadAction - ) => { - state.shouldAutoShowProgressImages = action.payload; + setShouldShowProgressInViewer: (state, action: PayloadAction) => { + state.shouldShowProgressInViewer = action.payload; }, shouldShowImageParametersChanged: ( state, @@ -173,7 +170,7 @@ export const { floatingProgressImageMoved, floatingProgressImageResized, setShouldShowProgressImages, - setShouldAutoShowProgressImages, + setShouldShowProgressInViewer, shouldShowImageParametersChanged, } = uiSlice.actions; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 59140cdfde..030ec4f1ce 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -31,6 +31,6 @@ export interface UIState { canvasTabAccordionState: number[]; floatingProgressImageRect: Rect; shouldShowProgressImages: boolean; - shouldAutoShowProgressImages: boolean; + shouldShowProgressInViewer: boolean; shouldShowImageParameters: boolean; } diff --git a/invokeai/frontend/web/src/theme/components/popover.ts b/invokeai/frontend/web/src/theme/components/popover.ts index 449f1d926c..c8d6ae20d8 100644 --- a/invokeai/frontend/web/src/theme/components/popover.ts +++ b/invokeai/frontend/web/src/theme/components/popover.ts @@ -20,9 +20,6 @@ const invokeAIContent = defineStyle((_props) => { minW: 'unset', width: 'unset', p: 4, - borderWidth: '2px', - borderStyle: 'solid', - borderColor: 'base.600', bg: 'base.800', }; });