mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): add floating image viewer
This commit is contained in:
parent
aab152a7e9
commit
cce3144c74
@ -89,6 +89,7 @@
|
|||||||
"react-konva": "^18.2.10",
|
"react-konva": "^18.2.10",
|
||||||
"react-redux": "9.1.0",
|
"react-redux": "9.1.0",
|
||||||
"react-resizable-panels": "^2.0.16",
|
"react-resizable-panels": "^2.0.16",
|
||||||
|
"react-rnd": "^10.4.10",
|
||||||
"react-select": "5.8.0",
|
"react-select": "5.8.0",
|
||||||
"react-use": "^17.5.0",
|
"react-use": "^17.5.0",
|
||||||
"react-virtuoso": "^4.7.5",
|
"react-virtuoso": "^4.7.5",
|
||||||
|
@ -122,6 +122,9 @@ dependencies:
|
|||||||
react-resizable-panels:
|
react-resizable-panels:
|
||||||
specifier: ^2.0.16
|
specifier: ^2.0.16
|
||||||
version: 2.0.16(react-dom@18.2.0)(react@18.2.0)
|
version: 2.0.16(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react-rnd:
|
||||||
|
specifier: ^10.4.10
|
||||||
|
version: 10.4.10(react-dom@18.2.0)(react@18.2.0)
|
||||||
react-select:
|
react-select:
|
||||||
specifier: 5.8.0
|
specifier: 5.8.0
|
||||||
version: 5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0)
|
version: 5.8.0(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@ -7385,6 +7388,11 @@ packages:
|
|||||||
requiresBuild: true
|
requiresBuild: true
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/clsx@1.2.1:
|
||||||
|
resolution: {integrity: sha512-EcR6r5a8bj6pu3ycsa/E/cKVGuTgZJZdsyUYHOksG/UHIiKfjxzRxYJpyVBwYaQeOvghal9fcc4PidlgzugAQg==}
|
||||||
|
engines: {node: '>=6'}
|
||||||
|
dev: false
|
||||||
|
|
||||||
/color-convert@1.9.3:
|
/color-convert@1.9.3:
|
||||||
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
resolution: {integrity: sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
@ -11200,6 +11208,16 @@ packages:
|
|||||||
unpipe: 1.0.0
|
unpipe: 1.0.0
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/re-resizable@6.9.14(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-2UbPrpezMr6gkHKNCRA/N6QGGU237SKOZ78yMHId204A/oXWSAREAIuGZNQ9qlrJosewzcsv2CphZH3u7hC6ng==}
|
||||||
|
peerDependencies:
|
||||||
|
react: ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||||
|
react-dom: ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||||
|
dependencies:
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-clientside-effect@1.2.6(react@18.2.0):
|
/react-clientside-effect@1.2.6(react@18.2.0):
|
||||||
resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==}
|
resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
@ -11253,6 +11271,18 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
scheduler: 0.23.0
|
scheduler: 0.23.0
|
||||||
|
|
||||||
|
/react-draggable@4.4.6(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-LtY5Xw1zTPqHkVmtM3X8MUOxNDOUhv/khTgBgrUvwaS064bwVvxT+q5El0uUFNx5IEPKXuRejr7UqLwBIg5pdw==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>= 16.3.0'
|
||||||
|
react-dom: '>= 16.3.0'
|
||||||
|
dependencies:
|
||||||
|
clsx: 1.2.1
|
||||||
|
prop-types: 15.8.1
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-dropzone@14.2.3(react@18.2.0):
|
/react-dropzone@14.2.3(react@18.2.0):
|
||||||
resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==}
|
resolution: {integrity: sha512-O3om8I+PkFKbxCukfIR3QAGftYXDZfOE2N1mr/7qebQJHs7U+/RSL/9xomJNpRg9kM5h9soQSdf0Gc7OHF5Fug==}
|
||||||
engines: {node: '>= 10.13'}
|
engines: {node: '>= 10.13'}
|
||||||
@ -11466,6 +11496,19 @@ packages:
|
|||||||
react-dom: 18.2.0(react@18.2.0)
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/react-rnd@10.4.10(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-YjQAgEeSbNUoOXSD9ZBvIiLVizFb+bNhpDk8DbIRHA557NW02CXbwsAeOTpJQnsdhEL+NP2I+Ssrwejqcodtjg==}
|
||||||
|
peerDependencies:
|
||||||
|
react: '>=16.3.0'
|
||||||
|
react-dom: '>=16.3.0'
|
||||||
|
dependencies:
|
||||||
|
re-resizable: 6.9.14(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
react-draggable: 4.4.6(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
tslib: 2.6.2
|
||||||
|
dev: false
|
||||||
|
|
||||||
/react-select@5.7.7(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0):
|
/react-select@5.7.7(@types/react@18.2.73)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==}
|
resolution: {integrity: sha512-HhashZZJDRlfF/AKj0a0Lnfs3sRdw/46VJIRd8IbB9/Ovr74+ZIwkAdSBjSPXsFMG+u72c5xShqwLSKIJllzqw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
@ -143,7 +143,8 @@
|
|||||||
"alpha": "Alpha",
|
"alpha": "Alpha",
|
||||||
"selected": "Selected",
|
"selected": "Selected",
|
||||||
"viewer": "Viewer",
|
"viewer": "Viewer",
|
||||||
"tab": "Tab"
|
"tab": "Tab",
|
||||||
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"controlnet": {
|
"controlnet": {
|
||||||
"controlAdapter_one": "Control Adapter",
|
"controlAdapter_one": "Control Adapter",
|
||||||
@ -365,7 +366,9 @@
|
|||||||
"bulkDownloadFailed": "Download Failed",
|
"bulkDownloadFailed": "Download Failed",
|
||||||
"problemDeletingImages": "Problem Deleting Images",
|
"problemDeletingImages": "Problem Deleting Images",
|
||||||
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
"problemDeletingImagesDesc": "One or more images could not be deleted",
|
||||||
"switchTo": "Switch to {{ tab }} (Z)"
|
"switchTo": "Switch to {{ tab }} (Z)",
|
||||||
|
"openFloatingViewer": "Open Floating Viewer",
|
||||||
|
"closeFloatingViewer": "Close Floating Viewer"
|
||||||
},
|
},
|
||||||
"hotkeys": {
|
"hotkeys": {
|
||||||
"searchHotkeys": "Search Hotkeys",
|
"searchHotkeys": "Search Hotkeys",
|
||||||
|
@ -12,6 +12,7 @@ import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
|||||||
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
import ChangeBoardModal from 'features/changeBoardModal/components/ChangeBoardModal';
|
||||||
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
import DeleteImageModal from 'features/deleteImageModal/components/DeleteImageModal';
|
||||||
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
import { DynamicPromptsModal } from 'features/dynamicPrompts/components/DynamicPromptsPreviewModal';
|
||||||
|
import { FloatingImageViewer } from 'features/gallery/components/ImageViewer/FloatingImageViewer';
|
||||||
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterModelsToast';
|
||||||
import { configChanged } from 'features/system/store/configSlice';
|
import { configChanged } from 'features/system/store/configSlice';
|
||||||
import { languageSelector } from 'features/system/store/systemSelectors';
|
import { languageSelector } from 'features/system/store/systemSelectors';
|
||||||
@ -96,6 +97,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage }: Props) => {
|
|||||||
<DynamicPromptsModal />
|
<DynamicPromptsModal />
|
||||||
<Toaster />
|
<Toaster />
|
||||||
<PreselectedImage selectedImage={selectedImage} />
|
<PreselectedImage selectedImage={selectedImage} />
|
||||||
|
<FloatingImageViewer />
|
||||||
</ErrorBoundary>
|
</ErrorBoundary>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -22,7 +22,13 @@ const selectLastSelectedImageName = createSelector(
|
|||||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||||
);
|
);
|
||||||
|
|
||||||
const CurrentImagePreview = () => {
|
type Props = {
|
||||||
|
isDragDisabled?: boolean;
|
||||||
|
isDropDisabled?: boolean;
|
||||||
|
withNextPrevButtons?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const CurrentImagePreview = ({ isDragDisabled = false, isDropDisabled = false, withNextPrevButtons = true }: Props) => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||||
@ -79,6 +85,8 @@ const CurrentImagePreview = () => {
|
|||||||
imageDTO={imageDTO}
|
imageDTO={imageDTO}
|
||||||
droppableData={droppableData}
|
droppableData={droppableData}
|
||||||
draggableData={draggableData}
|
draggableData={draggableData}
|
||||||
|
isDragDisabled={isDragDisabled}
|
||||||
|
isDropDisabled={isDropDisabled}
|
||||||
isUploadDisabled={true}
|
isUploadDisabled={true}
|
||||||
fitContainer
|
fitContainer
|
||||||
useThumbailFallback
|
useThumbailFallback
|
||||||
@ -106,7 +114,7 @@ const CurrentImagePreview = () => {
|
|||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
</AnimatePresence>
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{shouldShowNextPrevButtons && imageDTO && (
|
{withNextPrevButtons && shouldShowNextPrevButtons && imageDTO && (
|
||||||
<Box
|
<Box
|
||||||
as={motion.div}
|
as={motion.div}
|
||||||
key="nextPrevButtons"
|
key="nextPrevButtons"
|
||||||
|
@ -0,0 +1,178 @@
|
|||||||
|
import { Flex, IconButton, Spacer, Text, useShiftModifier } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||||
|
import { isFloatingImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { useCallback, useLayoutEffect, useRef } from 'react';
|
||||||
|
import { flushSync } from 'react-dom';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { PiHourglassBold, PiXBold } from 'react-icons/pi';
|
||||||
|
import { Rnd } from 'react-rnd';
|
||||||
|
|
||||||
|
const defaultDim = 256;
|
||||||
|
const maxDim = 512;
|
||||||
|
const defaultSize = { width: defaultDim, height: defaultDim + 24 };
|
||||||
|
const maxSize = { width: maxDim, height: maxDim + 24 };
|
||||||
|
const rndDefault = { x: 0, y: 0, ...defaultSize };
|
||||||
|
|
||||||
|
const rndStyles = {
|
||||||
|
zIndex: 11,
|
||||||
|
};
|
||||||
|
|
||||||
|
const enableResizing = {
|
||||||
|
top: false,
|
||||||
|
right: false,
|
||||||
|
bottom: false,
|
||||||
|
left: false,
|
||||||
|
topRight: false,
|
||||||
|
bottomRight: true,
|
||||||
|
bottomLeft: false,
|
||||||
|
topLeft: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FloatingImageViewerComponent = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const shift = useShiftModifier();
|
||||||
|
const rndRef = useRef<Rnd>(null);
|
||||||
|
const imagePreviewRef = useRef<HTMLDivElement>(null);
|
||||||
|
const onClose = useCallback(() => {
|
||||||
|
dispatch(isFloatingImageViewerOpenChanged(false));
|
||||||
|
}, [dispatch]);
|
||||||
|
|
||||||
|
const fitToScreen = useCallback(() => {
|
||||||
|
if (!imagePreviewRef.current || !rndRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const el = imagePreviewRef.current;
|
||||||
|
const rnd = rndRef.current;
|
||||||
|
|
||||||
|
const { top, right, bottom, left, width, height } = el.getBoundingClientRect();
|
||||||
|
const { innerWidth, innerHeight } = window;
|
||||||
|
|
||||||
|
const newPosition = rnd.getDraggablePosition();
|
||||||
|
|
||||||
|
if (top < 0) {
|
||||||
|
newPosition.y = 0;
|
||||||
|
}
|
||||||
|
if (left < 0) {
|
||||||
|
newPosition.x = 0;
|
||||||
|
}
|
||||||
|
if (bottom > innerHeight) {
|
||||||
|
newPosition.y = innerHeight - height;
|
||||||
|
}
|
||||||
|
if (right > innerWidth) {
|
||||||
|
newPosition.x = innerWidth - width;
|
||||||
|
}
|
||||||
|
rnd.updatePosition(newPosition);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const onDoubleClick = useCallback(() => {
|
||||||
|
if (!rndRef.current || !imagePreviewRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const { width, height } = imagePreviewRef.current.getBoundingClientRect();
|
||||||
|
if (width === defaultSize.width && height === defaultSize.height) {
|
||||||
|
rndRef.current.updateSize(maxSize);
|
||||||
|
} else {
|
||||||
|
rndRef.current.updateSize(defaultSize);
|
||||||
|
}
|
||||||
|
flushSync(fitToScreen);
|
||||||
|
}, [fitToScreen]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
window.addEventListener('resize', fitToScreen);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('resize', fitToScreen);
|
||||||
|
};
|
||||||
|
}, [fitToScreen]);
|
||||||
|
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
// Set the initial position
|
||||||
|
if (!imagePreviewRef.current || !rndRef.current) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const { width, height } = imagePreviewRef.current.getBoundingClientRect();
|
||||||
|
|
||||||
|
const initialPosition = {
|
||||||
|
// 54 = width of left-hand vertical bar of tab icons
|
||||||
|
// 430 = width of parameters panel
|
||||||
|
x: 54 + 430 / 2 - width / 2,
|
||||||
|
// 16 = just a reasonable bottom padding
|
||||||
|
y: window.innerHeight - height - 16,
|
||||||
|
};
|
||||||
|
|
||||||
|
rndRef.current.updatePosition(initialPosition);
|
||||||
|
}, [fitToScreen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Rnd
|
||||||
|
ref={rndRef}
|
||||||
|
default={rndDefault}
|
||||||
|
bounds="window"
|
||||||
|
lockAspectRatio={shift}
|
||||||
|
minWidth={defaultSize.width}
|
||||||
|
minHeight={defaultSize.height}
|
||||||
|
maxWidth={maxSize.width}
|
||||||
|
maxHeight={maxSize.height}
|
||||||
|
style={rndStyles}
|
||||||
|
enableResizing={enableResizing}
|
||||||
|
>
|
||||||
|
<Flex
|
||||||
|
ref={imagePreviewRef}
|
||||||
|
flexDir="column"
|
||||||
|
bg="base.850"
|
||||||
|
borderRadius="base"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
borderWidth={1}
|
||||||
|
shadow="dark-lg"
|
||||||
|
cursor="move"
|
||||||
|
>
|
||||||
|
<Flex bg="base.800" w="full" p={1} onDoubleClick={onDoubleClick}>
|
||||||
|
<Text fontSize="sm" fontWeight="semibold" color="base.300" ps={2}>
|
||||||
|
{t('common.viewer')}
|
||||||
|
</Text>
|
||||||
|
<Spacer />
|
||||||
|
<IconButton aria-label={t('common.close')} icon={<PiXBold />} size="sm" variant="link" onClick={onClose} />
|
||||||
|
</Flex>
|
||||||
|
<Flex p={2} w="full" h="full">
|
||||||
|
<CurrentImagePreview isDragDisabled={true} isDropDisabled={true} withNextPrevButtons={false} />
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
</Rnd>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const FloatingImageViewer = () => {
|
||||||
|
const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen);
|
||||||
|
|
||||||
|
if (!isOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return <FloatingImageViewerComponent />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ToggleFloatingImageViewerButton = () => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const isOpen = useAppSelector((s) => s.gallery.isFloatingImageViewerOpen);
|
||||||
|
|
||||||
|
const onToggle = useCallback(() => {
|
||||||
|
dispatch(isFloatingImageViewerOpenChanged(!isOpen));
|
||||||
|
}, [dispatch, isOpen]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<IconButton
|
||||||
|
tooltip={isOpen ? t('gallery.closeFloatingViewer') : t('gallery.openFloatingViewer')}
|
||||||
|
aria-label={isOpen ? t('gallery.closeFloatingViewer') : t('gallery.openFloatingViewer')}
|
||||||
|
icon={<PiHourglassBold fontSize={16} />}
|
||||||
|
size="sm"
|
||||||
|
onClick={onToggle}
|
||||||
|
variant="link"
|
||||||
|
colorScheme={isOpen ? 'invokeBlue' : 'base'}
|
||||||
|
boxSize={8}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
@ -23,6 +23,7 @@ const initialGalleryState: GalleryState = {
|
|||||||
limit: INITIAL_IMAGE_LIMIT,
|
limit: INITIAL_IMAGE_LIMIT,
|
||||||
offset: 0,
|
offset: 0,
|
||||||
isImageViewerOpen: false,
|
isImageViewerOpen: false,
|
||||||
|
isFloatingImageViewerOpen: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const gallerySlice = createSlice({
|
export const gallerySlice = createSlice({
|
||||||
@ -80,6 +81,9 @@ export const gallerySlice = createSlice({
|
|||||||
isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
|
isImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
state.isImageViewerOpen = action.payload;
|
state.isImageViewerOpen = action.payload;
|
||||||
},
|
},
|
||||||
|
isFloatingImageViewerOpenChanged: (state, action: PayloadAction<boolean>) => {
|
||||||
|
state.isFloatingImageViewerOpen = action.payload;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
extraReducers: (builder) => {
|
extraReducers: (builder) => {
|
||||||
builder.addCase(setActiveTab, (state) => {
|
builder.addCase(setActiveTab, (state) => {
|
||||||
@ -121,6 +125,7 @@ export const {
|
|||||||
moreImagesLoaded,
|
moreImagesLoaded,
|
||||||
alwaysShowImageSizeBadgeChanged,
|
alwaysShowImageSizeBadgeChanged,
|
||||||
isImageViewerOpenChanged,
|
isImageViewerOpenChanged,
|
||||||
|
isFloatingImageViewerOpenChanged,
|
||||||
} = gallerySlice.actions;
|
} = gallerySlice.actions;
|
||||||
|
|
||||||
const isAnyBoardDeleted = isAnyOf(
|
const isAnyBoardDeleted = isAnyOf(
|
||||||
|
@ -21,4 +21,5 @@ export type GalleryState = {
|
|||||||
limit: number;
|
limit: number;
|
||||||
alwaysShowImageSizeBadge: boolean;
|
alwaysShowImageSizeBadge: boolean;
|
||||||
isImageViewerOpen: boolean;
|
isImageViewerOpen: boolean;
|
||||||
|
isFloatingImageViewerOpen: boolean;
|
||||||
};
|
};
|
||||||
|
@ -4,6 +4,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|||||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
||||||
|
import { ToggleFloatingImageViewerButton } from 'features/gallery/components/ImageViewer/FloatingImageViewer';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||||
@ -223,6 +224,7 @@ const InvokeTabs = () => {
|
|||||||
</TabList>
|
</TabList>
|
||||||
<Spacer />
|
<Spacer />
|
||||||
<StatusIndicator />
|
<StatusIndicator />
|
||||||
|
<ToggleFloatingImageViewerButton />
|
||||||
{customNavComponent ? customNavComponent : <SettingsMenu />}
|
{customNavComponent ? customNavComponent : <SettingsMenu />}
|
||||||
</Flex>
|
</Flex>
|
||||||
<PanelGroup
|
<PanelGroup
|
||||||
|
Loading…
Reference in New Issue
Block a user