mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
feat(ui): revise app layout strategy, add interaction scopes for hotkeys
This commit is contained in:
parent
d6b3e6c07d
commit
f78f4ca25f
@ -9,6 +9,7 @@ import ImageUploadOverlay from 'common/components/ImageUploadOverlay';
|
|||||||
import { useClearStorage } from 'common/hooks/useClearStorage';
|
import { useClearStorage } from 'common/hooks/useClearStorage';
|
||||||
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
|
import { useFullscreenDropzone } from 'common/hooks/useFullscreenDropzone';
|
||||||
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
import { useGlobalHotkeys } from 'common/hooks/useGlobalHotkeys';
|
||||||
|
import { useScopeFocusWatcher } from 'common/hooks/interactionScopes';
|
||||||
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';
|
||||||
@ -16,7 +17,7 @@ import { useStarterModelsToast } from 'features/modelManagerV2/hooks/useStarterM
|
|||||||
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal';
|
||||||
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';
|
||||||
import InvokeTabs from 'features/ui/components/InvokeTabs';
|
import { AppContent } from 'features/ui/components/AppContent';
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
|
import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow';
|
||||||
@ -93,6 +94,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
|
|||||||
|
|
||||||
useStarterModelsToast();
|
useStarterModelsToast();
|
||||||
useSyncQueueStatus();
|
useSyncQueueStatus();
|
||||||
|
useScopeFocusWatcher();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
<ErrorBoundary onReset={handleReset} FallbackComponent={AppErrorBoundaryFallback}>
|
||||||
@ -105,7 +107,7 @@ const App = ({ config = DEFAULT_CONFIG, selectedImage, selectedWorkflowId, desti
|
|||||||
{...dropzone.getRootProps()}
|
{...dropzone.getRootProps()}
|
||||||
>
|
>
|
||||||
<input {...dropzone.getInputProps()} />
|
<input {...dropzone.getInputProps()} />
|
||||||
<InvokeTabs />
|
<AppContent />
|
||||||
<AnimatePresence>
|
<AnimatePresence>
|
||||||
{dropzone.isDragActive && isHandlingUpload && (
|
{dropzone.isDragActive && isHandlingUpload && (
|
||||||
<ImageUploadOverlay dropzone={dropzone} setIsHandlingUpload={setIsHandlingUpload} />
|
<ImageUploadOverlay dropzone={dropzone} setIsHandlingUpload={setIsHandlingUpload} />
|
||||||
|
163
invokeai/frontend/web/src/common/hooks/interactionScopes.ts
Normal file
163
invokeai/frontend/web/src/common/hooks/interactionScopes.ts
Normal file
@ -0,0 +1,163 @@
|
|||||||
|
import { logger } from 'app/logging/logger';
|
||||||
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||||
|
import { objectKeys } from 'common/util/objectKeys';
|
||||||
|
import { isEqual } from 'lodash-es';
|
||||||
|
import type { Atom } from 'nanostores';
|
||||||
|
import { atom, computed } from 'nanostores';
|
||||||
|
import type { RefObject } from 'react';
|
||||||
|
import { useEffect, useMemo } from 'react';
|
||||||
|
|
||||||
|
const log = logger('system');
|
||||||
|
|
||||||
|
const _INTERACTION_SCOPES = ['gallery', 'canvas', 'workflows', 'imageViewer'] as const;
|
||||||
|
|
||||||
|
type InteractionScope = (typeof _INTERACTION_SCOPES)[number];
|
||||||
|
|
||||||
|
export const $activeScopes = atom<Set<InteractionScope>>(new Set());
|
||||||
|
|
||||||
|
type InteractionScopeData = {
|
||||||
|
targets: Set<HTMLElement>;
|
||||||
|
$isActive: Atom<boolean>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const INTERACTION_SCOPES: Record<InteractionScope, InteractionScopeData> = _INTERACTION_SCOPES.reduce(
|
||||||
|
(acc, region) => {
|
||||||
|
acc[region] = {
|
||||||
|
targets: new Set(),
|
||||||
|
$isActive: computed($activeScopes, (activeScopes) => activeScopes.has(region)),
|
||||||
|
};
|
||||||
|
return acc;
|
||||||
|
},
|
||||||
|
{} as Record<InteractionScope, InteractionScopeData>
|
||||||
|
);
|
||||||
|
|
||||||
|
const formatScopes = (interactionScopes: Set<InteractionScope>) => {
|
||||||
|
if (interactionScopes.size === 0) {
|
||||||
|
return 'none';
|
||||||
|
}
|
||||||
|
return Array.from(interactionScopes).join(', ');
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addScope = (scope: InteractionScope) => {
|
||||||
|
const currentScopes = $activeScopes.get();
|
||||||
|
if (currentScopes.has(scope)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newScopes = new Set(currentScopes);
|
||||||
|
newScopes.add(scope);
|
||||||
|
$activeScopes.set(newScopes);
|
||||||
|
log.trace(`Added scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeScope = (scope: InteractionScope) => {
|
||||||
|
const currentScopes = $activeScopes.get();
|
||||||
|
if (!currentScopes.has(scope)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const newScopes = new Set(currentScopes);
|
||||||
|
newScopes.delete(scope);
|
||||||
|
$activeScopes.set(newScopes);
|
||||||
|
log.trace(`Removed scope ${scope}: ${formatScopes($activeScopes.get())}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const setScopes = (scopes: InteractionScope[]) => {
|
||||||
|
const newScopes = new Set(scopes);
|
||||||
|
$activeScopes.set(newScopes);
|
||||||
|
log.trace(`Set scopes: ${formatScopes($activeScopes.get())}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const clearScopes = () => {
|
||||||
|
$activeScopes.set(new Set());
|
||||||
|
log.trace(`Cleared scopes`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScopeOnFocus = (scope: InteractionScope, ref: RefObject<HTMLElement>) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const element = ref.current;
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
INTERACTION_SCOPES[scope].targets.add(element);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
INTERACTION_SCOPES[scope].targets.delete(element);
|
||||||
|
};
|
||||||
|
}, [ref, scope]);
|
||||||
|
};
|
||||||
|
|
||||||
|
type UseScopeOnMountOptions = {
|
||||||
|
mount?: boolean;
|
||||||
|
unmount?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const defaultUseScopeOnMountOptions: UseScopeOnMountOptions = {
|
||||||
|
mount: true,
|
||||||
|
unmount: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScopeOnMount = (scope: InteractionScope, options?: UseScopeOnMountOptions) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const { mount, unmount } = { ...defaultUseScopeOnMountOptions, ...options };
|
||||||
|
|
||||||
|
if (mount) {
|
||||||
|
addScope(scope);
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (unmount) {
|
||||||
|
removeScope(scope);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, [options, scope]);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScopeImperativeApi = (scope: InteractionScope) => {
|
||||||
|
const api = useMemo(() => {
|
||||||
|
return {
|
||||||
|
add: () => {
|
||||||
|
addScope(scope);
|
||||||
|
},
|
||||||
|
remove: () => {
|
||||||
|
removeScope(scope);
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [scope]);
|
||||||
|
|
||||||
|
return api;
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleFocusEvent = (_event: FocusEvent) => {
|
||||||
|
const activeElement = document.activeElement;
|
||||||
|
if (!(activeElement instanceof HTMLElement)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newActiveScopes = new Set<InteractionScope>();
|
||||||
|
|
||||||
|
for (const scope of objectKeys(INTERACTION_SCOPES)) {
|
||||||
|
for (const element of INTERACTION_SCOPES[scope].targets) {
|
||||||
|
if (element.contains(activeElement)) {
|
||||||
|
newActiveScopes.add(scope);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const oldActiveScopes = $activeScopes.get();
|
||||||
|
if (!isEqual(oldActiveScopes, newActiveScopes)) {
|
||||||
|
$activeScopes.set(newActiveScopes);
|
||||||
|
log.trace(`Scopes changed: ${formatScopes($activeScopes.get())}`);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const useScopeFocusWatcher = () => {
|
||||||
|
useAssertSingleton('useScopeFocusWatcher');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
window.addEventListener('focus', handleFocusEvent, true);
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('focus', handleFocusEvent, true);
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
};
|
@ -1,4 +1,5 @@
|
|||||||
import { useAppDispatch } from 'app/store/storeHooks';
|
import { useAppDispatch } from 'app/store/storeHooks';
|
||||||
|
import { addScope, removeScope, setScopes } from 'common/hooks/interactionScopes';
|
||||||
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
import { useCancelCurrentQueueItem } from 'features/queue/hooks/useCancelCurrentQueueItem';
|
||||||
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
import { useClearQueue } from 'features/queue/hooks/useClearQueue';
|
||||||
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
import { useQueueBack } from 'features/queue/hooks/useQueueBack';
|
||||||
@ -16,7 +17,7 @@ export const useGlobalHotkeys = () => {
|
|||||||
['ctrl+enter', 'meta+enter'],
|
['ctrl+enter', 'meta+enter'],
|
||||||
queueBack,
|
queueBack,
|
||||||
{
|
{
|
||||||
enabled: () => !isDisabledQueueBack && !isLoadingQueueBack,
|
enabled: !isDisabledQueueBack && !isLoadingQueueBack,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||||
},
|
},
|
||||||
@ -29,7 +30,7 @@ export const useGlobalHotkeys = () => {
|
|||||||
['ctrl+shift+enter', 'meta+shift+enter'],
|
['ctrl+shift+enter', 'meta+shift+enter'],
|
||||||
queueFront,
|
queueFront,
|
||||||
{
|
{
|
||||||
enabled: () => !isDisabledQueueFront && !isLoadingQueueFront,
|
enabled: !isDisabledQueueFront && !isLoadingQueueFront,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
enableOnFormTags: ['input', 'textarea', 'select'],
|
enableOnFormTags: ['input', 'textarea', 'select'],
|
||||||
},
|
},
|
||||||
@ -46,7 +47,7 @@ export const useGlobalHotkeys = () => {
|
|||||||
['shift+x'],
|
['shift+x'],
|
||||||
cancelQueueItem,
|
cancelQueueItem,
|
||||||
{
|
{
|
||||||
enabled: () => !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
|
enabled: !isDisabledCancelQueueItem && !isLoadingCancelQueueItem,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem]
|
[cancelQueueItem, isDisabledCancelQueueItem, isLoadingCancelQueueItem]
|
||||||
@ -58,7 +59,7 @@ export const useGlobalHotkeys = () => {
|
|||||||
['ctrl+shift+x', 'meta+shift+x'],
|
['ctrl+shift+x', 'meta+shift+x'],
|
||||||
clearQueue,
|
clearQueue,
|
||||||
{
|
{
|
||||||
enabled: () => !isDisabledClearQueue && !isLoadingClearQueue,
|
enabled: !isDisabledClearQueue && !isLoadingClearQueue,
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
},
|
},
|
||||||
[clearQueue, isDisabledClearQueue, isLoadingClearQueue]
|
[clearQueue, isDisabledClearQueue, isLoadingClearQueue]
|
||||||
@ -68,6 +69,8 @@ export const useGlobalHotkeys = () => {
|
|||||||
'1',
|
'1',
|
||||||
() => {
|
() => {
|
||||||
dispatch(setActiveTab('generation'));
|
dispatch(setActiveTab('generation'));
|
||||||
|
addScope('canvas');
|
||||||
|
removeScope('workflows');
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
@ -75,25 +78,39 @@ export const useGlobalHotkeys = () => {
|
|||||||
useHotkeys(
|
useHotkeys(
|
||||||
'2',
|
'2',
|
||||||
() => {
|
() => {
|
||||||
dispatch(setActiveTab('workflows'));
|
dispatch(setActiveTab('upscaling'));
|
||||||
|
removeScope('canvas');
|
||||||
|
removeScope('workflows');
|
||||||
},
|
},
|
||||||
[dispatch]
|
[dispatch]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'3',
|
'3',
|
||||||
|
() => {
|
||||||
|
dispatch(setActiveTab('workflows'));
|
||||||
|
removeScope('canvas');
|
||||||
|
addScope('workflows');
|
||||||
|
},
|
||||||
|
[dispatch]
|
||||||
|
);
|
||||||
|
|
||||||
|
useHotkeys(
|
||||||
|
'4',
|
||||||
() => {
|
() => {
|
||||||
if (isModelManagerEnabled) {
|
if (isModelManagerEnabled) {
|
||||||
dispatch(setActiveTab('models'));
|
dispatch(setActiveTab('models'));
|
||||||
|
setScopes([]);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[dispatch, isModelManagerEnabled]
|
[dispatch, isModelManagerEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
isModelManagerEnabled ? '4' : '3',
|
isModelManagerEnabled ? '5' : '4',
|
||||||
() => {
|
() => {
|
||||||
dispatch(setActiveTab('queue'));
|
dispatch(setActiveTab('queue'));
|
||||||
|
setScopes([]);
|
||||||
},
|
},
|
||||||
[dispatch, isModelManagerEnabled]
|
[dispatch, isModelManagerEnabled]
|
||||||
);
|
);
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import IAIDroppable from 'common/components/IAIDroppable';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import type { AddLayerFromImageDropData } from 'features/dnd/types';
|
import type { AddLayerFromImageDropData } from 'features/dnd/types';
|
||||||
|
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const addLayerFromImageDropData: AddLayerFromImageDropData = {
|
const addLayerFromImageDropData: AddLayerFromImageDropData = {
|
||||||
@ -9,6 +10,12 @@ const addLayerFromImageDropData: AddLayerFromImageDropData = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const CanvasDropArea = memo(() => {
|
export const CanvasDropArea = memo(() => {
|
||||||
|
const isImageViewerOpen = useIsImageViewerOpen();
|
||||||
|
|
||||||
|
if (isImageViewerOpen) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
||||||
<IAIDroppable dropLabel="Create Layer" data={addLayerFromImageDropData} />
|
<IAIDroppable dropLabel="Create Layer" data={addLayerFromImageDropData} />
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { $shift, IconButton } from '@invoke-ai/ui-library';
|
import { $shift, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||||
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
import { $canvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||||
import { memo, useCallback } from 'react';
|
import { memo, useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
@ -9,6 +10,7 @@ import { PiArrowCounterClockwiseBold } from 'react-icons/pi';
|
|||||||
export const CanvasResetViewButton = memo(() => {
|
export const CanvasResetViewButton = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const canvasManager = useStore($canvasManager);
|
const canvasManager = useStore($canvasManager);
|
||||||
|
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||||
|
|
||||||
const resetZoom = useCallback(() => {
|
const resetZoom = useCallback(() => {
|
||||||
if (!canvasManager) {
|
if (!canvasManager) {
|
||||||
@ -32,8 +34,8 @@ export const CanvasResetViewButton = memo(() => {
|
|||||||
}
|
}
|
||||||
}, [resetView, resetZoom]);
|
}, [resetView, resetZoom]);
|
||||||
|
|
||||||
useHotkeys('r', resetView);
|
useHotkeys('r', resetView, { enabled: isCanvasActive }, [isCanvasActive]);
|
||||||
useHotkeys('shift+r', resetZoom);
|
useHotkeys('shift+r', resetZoom, { enabled: isCanvasActive }, [isCanvasActive]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<IconButton
|
<IconButton
|
||||||
|
@ -1,20 +1,20 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import type { Meta, StoryObj } from '@storybook/react';
|
import type { Meta, StoryObj } from '@storybook/react';
|
||||||
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
|
|
||||||
const meta: Meta<typeof ControlLayersEditor> = {
|
const meta: Meta<typeof CanvasEditor> = {
|
||||||
title: 'Feature/ControlLayers',
|
title: 'Feature/ControlLayers',
|
||||||
tags: ['autodocs'],
|
tags: ['autodocs'],
|
||||||
component: ControlLayersEditor,
|
component: CanvasEditor,
|
||||||
};
|
};
|
||||||
|
|
||||||
export default meta;
|
export default meta;
|
||||||
type Story = StoryObj<typeof ControlLayersEditor>;
|
type Story = StoryObj<typeof CanvasEditor>;
|
||||||
|
|
||||||
const Component = () => {
|
const Component = () => {
|
||||||
return (
|
return (
|
||||||
<Flex w={1500} h={1500}>
|
<Flex w={1500} h={1500}>
|
||||||
<ControlLayersEditor />
|
<CanvasEditor />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,18 +1,27 @@
|
|||||||
/* eslint-disable i18next/no-literal-string */
|
/* eslint-disable i18next/no-literal-string */
|
||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||||
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
import { CanvasDropArea } from 'features/controlLayers/components/CanvasDropArea';
|
||||||
import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar';
|
import { ControlLayersToolbar } from 'features/controlLayers/components/ControlLayersToolbar';
|
||||||
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
import { StageComponent } from 'features/controlLayers/components/StageComponent';
|
||||||
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
import { StagingAreaToolbar } from 'features/controlLayers/components/StagingArea/StagingAreaToolbar';
|
||||||
import { memo } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
|
|
||||||
|
export const CanvasEditor = memo(() => {
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useScopeOnFocus('canvas', ref);
|
||||||
|
|
||||||
export const ControlLayersEditor = memo(() => {
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
tabIndex={-1}
|
||||||
|
ref={ref}
|
||||||
|
layerStyle="first"
|
||||||
|
p={2}
|
||||||
|
borderRadius="base"
|
||||||
position="relative"
|
position="relative"
|
||||||
flexDirection="column"
|
flexDirection="column"
|
||||||
height="100%"
|
height="full"
|
||||||
width="100%"
|
width="full"
|
||||||
gap={2}
|
gap={2}
|
||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
@ -22,12 +31,9 @@ export const ControlLayersEditor = memo(() => {
|
|||||||
<Flex position="absolute" bottom={2} gap={2} align="center" justify="center">
|
<Flex position="absolute" bottom={2} gap={2} align="center" justify="center">
|
||||||
<StagingAreaToolbar />
|
<StagingAreaToolbar />
|
||||||
</Flex>
|
</Flex>
|
||||||
{/* <Flex position="absolute" top={0} right={0} bottom={0} left={0} align="center" justify="center">
|
|
||||||
<CanvasResizer />
|
|
||||||
</Flex> */}
|
|
||||||
<CanvasDropArea />
|
<CanvasDropArea />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
ControlLayersEditor.displayName = 'ControlLayersEditor';
|
CanvasEditor.displayName = 'CanvasEditor';
|
||||||
|
@ -80,7 +80,7 @@ export const StageComponent = memo(({ asPreview = false }: Props) => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="relative" w="full" h="full">
|
<Flex position="relative" w="full" h="full" bg={canvasBackgroundStyle === 'checkerboard' ? 'base.900' : 'base.850'}>
|
||||||
{canvasBackgroundStyle === 'checkerboard' && (
|
{canvasBackgroundStyle === 'checkerboard' && (
|
||||||
<Flex
|
<Flex
|
||||||
position="absolute"
|
position="absolute"
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||||
import {
|
import {
|
||||||
$shouldShowStagedImage,
|
$shouldShowStagedImage,
|
||||||
sessionNextStagedImageSelected,
|
sessionNextStagedImageSelected,
|
||||||
@ -37,13 +38,13 @@ StagingAreaToolbar.displayName = 'StagingAreaToolbar';
|
|||||||
|
|
||||||
export const StagingAreaToolbarContent = memo(() => {
|
export const StagingAreaToolbarContent = memo(() => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const stagingArea = useAppSelector((s) => s.canvasV2.session);
|
const session = useAppSelector((s) => s.canvasV2.session);
|
||||||
const shouldShowStagedImage = useStore($shouldShowStagedImage);
|
const shouldShowStagedImage = useStore($shouldShowStagedImage);
|
||||||
const images = useMemo(() => stagingArea.stagedImages, [stagingArea]);
|
const images = useMemo(() => session.stagedImages, [session]);
|
||||||
const selectedImage = useMemo(() => {
|
const selectedImage = useMemo(() => {
|
||||||
return images[stagingArea.selectedStagedImageIndex] ?? null;
|
return images[session.selectedStagedImageIndex] ?? null;
|
||||||
}, [images, stagingArea.selectedStagedImageIndex]);
|
}, [images, session.selectedStagedImageIndex]);
|
||||||
|
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||||
// const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
|
// const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
|
||||||
|
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -60,8 +61,8 @@ export const StagingAreaToolbarContent = memo(() => {
|
|||||||
if (!selectedImage) {
|
if (!selectedImage) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
dispatch(sessionStagingAreaImageAccepted({ index: stagingArea.selectedStagedImageIndex }));
|
dispatch(sessionStagingAreaImageAccepted({ index: session.selectedStagedImageIndex }));
|
||||||
}, [dispatch, selectedImage, stagingArea.selectedStagedImageIndex]);
|
}, [dispatch, selectedImage, session.selectedStagedImageIndex]);
|
||||||
|
|
||||||
const onDiscardOne = useCallback(() => {
|
const onDiscardOne = useCallback(() => {
|
||||||
if (!selectedImage) {
|
if (!selectedImage) {
|
||||||
@ -70,9 +71,9 @@ export const StagingAreaToolbarContent = memo(() => {
|
|||||||
if (images.length === 1) {
|
if (images.length === 1) {
|
||||||
dispatch(sessionStagingAreaReset());
|
dispatch(sessionStagingAreaReset());
|
||||||
} else {
|
} else {
|
||||||
dispatch(sessionStagedImageDiscarded({ index: stagingArea.selectedStagedImageIndex }));
|
dispatch(sessionStagedImageDiscarded({ index: session.selectedStagedImageIndex }));
|
||||||
}
|
}
|
||||||
}, [selectedImage, images.length, dispatch, stagingArea.selectedStagedImageIndex]);
|
}, [selectedImage, images.length, dispatch, session.selectedStagedImageIndex]);
|
||||||
|
|
||||||
const onDiscardAll = useCallback(() => {
|
const onDiscardAll = useCallback(() => {
|
||||||
dispatch(sessionStagingAreaReset());
|
dispatch(sessionStagingAreaReset());
|
||||||
@ -95,25 +96,43 @@ export const StagingAreaToolbarContent = memo(() => {
|
|||||||
]
|
]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(['left'], onPrev, {
|
useHotkeys(
|
||||||
|
['left'],
|
||||||
|
onPrev,
|
||||||
|
{
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
});
|
enabled: isCanvasActive,
|
||||||
|
},
|
||||||
|
[isCanvasActive]
|
||||||
|
);
|
||||||
|
|
||||||
useHotkeys(['right'], onNext, {
|
useHotkeys(
|
||||||
|
['right'],
|
||||||
|
onNext,
|
||||||
|
{
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
});
|
enabled: isCanvasActive,
|
||||||
|
},
|
||||||
|
[isCanvasActive]
|
||||||
|
);
|
||||||
|
|
||||||
useHotkeys(['enter'], onAccept, {
|
useHotkeys(
|
||||||
|
['enter'],
|
||||||
|
onAccept,
|
||||||
|
{
|
||||||
preventDefault: true,
|
preventDefault: true,
|
||||||
});
|
enabled: isCanvasActive,
|
||||||
|
},
|
||||||
|
[isCanvasActive]
|
||||||
|
);
|
||||||
|
|
||||||
const counterText = useMemo(() => {
|
const counterText = useMemo(() => {
|
||||||
if (images.length > 0) {
|
if (images.length > 0) {
|
||||||
return `${(stagingArea.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`;
|
return `${(session.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`;
|
||||||
} else {
|
} else {
|
||||||
return `0 of 0`;
|
return `0 of 0`;
|
||||||
}
|
}
|
||||||
}, [images.length, stagingArea.selectedStagedImageIndex]);
|
}, [images.length, session.selectedStagedImageIndex]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,5 +1,6 @@
|
|||||||
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
|
import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from '@invoke-ai/ui-library';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||||
@ -18,19 +19,21 @@ import GallerySettingsPopover from './GallerySettingsPopover/GallerySettingsPopo
|
|||||||
|
|
||||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||||
|
|
||||||
const ImageGalleryContent = () => {
|
const GalleryPanelContent = () => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useScopeOnFocus('gallery', ref);
|
||||||
|
|
||||||
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
const boardsListPanelOptions = useMemo<UsePanelOptions>(
|
||||||
() => ({
|
() => ({
|
||||||
|
id: 'boards-list-panel',
|
||||||
unit: 'pixels',
|
unit: 'pixels',
|
||||||
minSize: 128,
|
minSize: 128,
|
||||||
defaultSize: 256,
|
defaultSize: 20,
|
||||||
fallbackMinSizePct: 20,
|
|
||||||
panelGroupRef,
|
panelGroupRef,
|
||||||
panelGroupDirection: 'vertical',
|
panelGroupDirection: 'vertical',
|
||||||
}),
|
}),
|
||||||
@ -55,7 +58,7 @@ const ImageGalleryContent = () => {
|
|||||||
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
}, [boardSearchText.length, boardSearchDisclosure, boardsListPanel, dispatch]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="relative" flexDirection="column" h="full" w="full" pt={2}>
|
<Flex ref={ref} position="relative" flexDirection="column" h="full" w="full" pt={2} tabIndex={-1}>
|
||||||
<Flex alignItems="center" gap={0}>
|
<Flex alignItems="center" gap={0}>
|
||||||
<GalleryHeader />
|
<GalleryHeader />
|
||||||
<Flex alignItems="center" justifyContent="space-between" w="full">
|
<Flex alignItems="center" justifyContent="space-between" w="full">
|
||||||
@ -90,15 +93,7 @@ const ImageGalleryContent = () => {
|
|||||||
</Flex>
|
</Flex>
|
||||||
|
|
||||||
<PanelGroup ref={panelGroupRef} direction="vertical">
|
<PanelGroup ref={panelGroupRef} direction="vertical">
|
||||||
<Panel
|
<Panel collapsible {...boardsListPanel.panelProps}>
|
||||||
id="boards-list-panel"
|
|
||||||
ref={boardsListPanel.ref}
|
|
||||||
defaultSize={boardsListPanel.defaultSize}
|
|
||||||
minSize={boardsListPanel.minSize}
|
|
||||||
onCollapse={boardsListPanel.onCollapse}
|
|
||||||
onExpand={boardsListPanel.onExpand}
|
|
||||||
collapsible
|
|
||||||
>
|
|
||||||
<Flex flexDir="column" w="full" h="full">
|
<Flex flexDir="column" w="full" h="full">
|
||||||
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
<Collapse in={boardSearchDisclosure.isOpen} style={COLLAPSE_STYLES}>
|
||||||
<Box w="full" pt={2}>
|
<Box w="full" pt={2}>
|
||||||
@ -109,11 +104,7 @@ const ImageGalleryContent = () => {
|
|||||||
<BoardsListWrapper />
|
<BoardsListWrapper />
|
||||||
</Flex>
|
</Flex>
|
||||||
</Panel>
|
</Panel>
|
||||||
<ResizeHandle
|
<ResizeHandle id="gallery-panel-handle" orientation="horizontal" {...boardsListPanel.resizeHandleProps} />
|
||||||
id="gallery-panel-handle"
|
|
||||||
orientation="horizontal"
|
|
||||||
onDoubleClick={boardsListPanel.onDoubleClickHandle}
|
|
||||||
/>
|
|
||||||
<Panel id="gallery-wrapper-panel" minSize={20}>
|
<Panel id="gallery-wrapper-panel" minSize={20}>
|
||||||
<Gallery />
|
<Gallery />
|
||||||
</Panel>
|
</Panel>
|
||||||
@ -122,4 +113,4 @@ const ImageGalleryContent = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(ImageGalleryContent);
|
export default memo(GalleryPanelContent);
|
@ -1,16 +1,25 @@
|
|||||||
import { Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library';
|
import { Tag, TagCloseButton, TagLabel } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { $activeScopes } from 'common/hooks/interactionScopes';
|
||||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||||
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
import { selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||||
|
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
|
||||||
|
import { computed } from 'nanostores';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
const $isSelectAllEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||||
|
return activeScopes.has('gallery') && !activeScopes.has('workflows') && isGalleryPanelOpen;
|
||||||
|
});
|
||||||
|
|
||||||
export const GallerySelectionCountTag = () => {
|
export const GallerySelectionCountTag = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const { selection } = useAppSelector((s) => s.gallery);
|
const { selection } = useAppSelector((s) => s.gallery);
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const { imageDTOs } = useGalleryImages();
|
const { imageDTOs } = useGalleryImages();
|
||||||
|
const isSelectAllEnabled = useStore($isSelectAllEnabled);
|
||||||
|
|
||||||
const onClearSelection = useCallback(() => {
|
const onClearSelection = useCallback(() => {
|
||||||
dispatch(selectionChanged([]));
|
dispatch(selectionChanged([]));
|
||||||
@ -20,7 +29,16 @@ export const GallerySelectionCountTag = () => {
|
|||||||
dispatch(selectionChanged([...selection, ...imageDTOs]));
|
dispatch(selectionChanged([...selection, ...imageDTOs]));
|
||||||
}, [dispatch, selection, imageDTOs]);
|
}, [dispatch, selection, imageDTOs]);
|
||||||
|
|
||||||
useHotkeys(['ctrl+a', 'meta+a'], onSelectPage, { preventDefault: true }, [onSelectPage]);
|
useHotkeys(['ctrl+a', 'meta+a'], onSelectPage, { preventDefault: true, enabled: isSelectAllEnabled }, [
|
||||||
|
onSelectPage,
|
||||||
|
isSelectAllEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
|
useHotkeys('esc', onClearSelection, { enabled: selection.length > 0 && isSelectAllEnabled }, [
|
||||||
|
onClearSelection,
|
||||||
|
selection,
|
||||||
|
isSelectAllEnabled,
|
||||||
|
]);
|
||||||
|
|
||||||
if (selection.length <= 1) {
|
if (selection.length <= 1) {
|
||||||
return null;
|
return null;
|
||||||
|
@ -4,6 +4,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
|
|||||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||||
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddleware/listeners/addAdHocPostProcessingRequestedListener';
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||||
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
import { DeleteImageButton } from 'features/deleteImageModal/components/DeleteImageButton';
|
||||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||||
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
import SingleSelectionMenuItems from 'features/gallery/components/ImageContextMenu/SingleSelectionMenuItems';
|
||||||
@ -46,7 +47,7 @@ const CurrentImageButtons = () => {
|
|||||||
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
const isUpscalingEnabled = useFeatureStatus('upscaling');
|
||||||
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
const isQueueMutationInProgress = useIsQueueMutationInProgress();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
const isImageViewerActive = useStore(INTERACTION_SCOPES.imageViewer.$isActive);
|
||||||
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
const { currentData: imageDTO } = useGetImageDTOQuery(lastSelectedImage?.image_name ?? skipToken);
|
||||||
|
|
||||||
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
|
const { recallAll, remix, recallSeed, recallPrompts, hasMetadata, hasSeed, hasPrompts, isLoadingMetadata } =
|
||||||
@ -61,18 +62,9 @@ const CurrentImageButtons = () => {
|
|||||||
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
|
getAndLoadEmbeddedWorkflow(lastSelectedImage.image_name);
|
||||||
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
|
}, [getAndLoadEmbeddedWorkflow, lastSelectedImage]);
|
||||||
|
|
||||||
useHotkeys('w', handleLoadWorkflow, [lastSelectedImage]);
|
|
||||||
useHotkeys('a', recallAll, [recallAll]);
|
|
||||||
useHotkeys('s', recallSeed, [recallSeed]);
|
|
||||||
useHotkeys('p', recallPrompts, [recallPrompts]);
|
|
||||||
useHotkeys('r', remix, [remix]);
|
|
||||||
|
|
||||||
const handleUseSize = useCallback(() => {
|
const handleUseSize = useCallback(() => {
|
||||||
parseAndRecallImageDimensions(lastSelectedImage);
|
parseAndRecallImageDimensions(lastSelectedImage);
|
||||||
}, [lastSelectedImage]);
|
}, [lastSelectedImage]);
|
||||||
|
|
||||||
useHotkeys('d', handleUseSize, [handleUseSize]);
|
|
||||||
|
|
||||||
const handleSendToImageToImage = useCallback(() => {
|
const handleSendToImageToImage = useCallback(() => {
|
||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
@ -81,9 +73,6 @@ const CurrentImageButtons = () => {
|
|||||||
dispatch(sentImageToImg2Img());
|
dispatch(sentImageToImg2Img());
|
||||||
dispatch(setActiveTab('generation'));
|
dispatch(setActiveTab('generation'));
|
||||||
}, [dispatch, imageDTO]);
|
}, [dispatch, imageDTO]);
|
||||||
|
|
||||||
useHotkeys('shift+i', handleSendToImageToImage, [imageDTO]);
|
|
||||||
|
|
||||||
const handleClickUpscale = useCallback(() => {
|
const handleClickUpscale = useCallback(() => {
|
||||||
if (!imageDTO) {
|
if (!imageDTO) {
|
||||||
return;
|
return;
|
||||||
@ -98,24 +87,21 @@ const CurrentImageButtons = () => {
|
|||||||
dispatch(imagesToDeleteSelected(selection));
|
dispatch(imagesToDeleteSelected(selection));
|
||||||
}, [dispatch, imageDTO, selection]);
|
}, [dispatch, imageDTO, selection]);
|
||||||
|
|
||||||
|
useHotkeys('w', handleLoadWorkflow, { enabled: isImageViewerActive }, [lastSelectedImage, isImageViewerActive]);
|
||||||
|
useHotkeys('a', recallAll, { enabled: isImageViewerActive }, [recallAll, isImageViewerActive]);
|
||||||
|
useHotkeys('s', recallSeed, { enabled: isImageViewerActive }, [recallSeed, isImageViewerActive]);
|
||||||
|
useHotkeys('p', recallPrompts, { enabled: isImageViewerActive }, [recallPrompts, isImageViewerActive]);
|
||||||
|
useHotkeys('r', remix, { enabled: isImageViewerActive }, [remix, isImageViewerActive]);
|
||||||
|
useHotkeys('d', handleUseSize, { enabled: isImageViewerActive }, [handleUseSize, isImageViewerActive]);
|
||||||
|
useHotkeys('shift+i', handleSendToImageToImage, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]);
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
'Shift+U',
|
'Shift+U',
|
||||||
() => {
|
handleClickUpscale,
|
||||||
handleClickUpscale();
|
{ enabled: Boolean(isUpscalingEnabled && isImageViewerActive && isConnected) },
|
||||||
},
|
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected, isImageViewerActive]
|
||||||
{
|
|
||||||
enabled: () => Boolean(isUpscalingEnabled && !shouldDisableToolbarButtons && isConnected),
|
|
||||||
},
|
|
||||||
[isUpscalingEnabled, imageDTO, shouldDisableToolbarButtons, isConnected]
|
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys('delete', handleDelete, { enabled: isImageViewerActive }, [imageDTO, isImageViewerActive]);
|
||||||
'delete',
|
|
||||||
() => {
|
|
||||||
handleDelete();
|
|
||||||
},
|
|
||||||
[dispatch, imageDTO]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
@ -1,21 +1,14 @@
|
|||||||
import { Flex } from '@invoke-ai/ui-library';
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import IAIDroppable from 'common/components/IAIDroppable';
|
import IAIDroppable from 'common/components/IAIDroppable';
|
||||||
import type { CurrentImageDropData, SelectForCompareDropData } from 'features/dnd/types';
|
import type { SelectForCompareDropData } from 'features/dnd/types';
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
|
||||||
import { memo, useMemo } from 'react';
|
import { memo, useMemo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
import { selectComparisonImages } from './common';
|
import { selectComparisonImages } from './common';
|
||||||
|
|
||||||
const setCurrentImageDropData: CurrentImageDropData = {
|
|
||||||
id: 'current-image',
|
|
||||||
actionType: 'SET_CURRENT_IMAGE',
|
|
||||||
};
|
|
||||||
|
|
||||||
export const ImageComparisonDroppable = memo(() => {
|
export const ImageComparisonDroppable = memo(() => {
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
const { firstImage, secondImage } = useAppSelector(selectComparisonImages);
|
const { firstImage, secondImage } = useAppSelector(selectComparisonImages);
|
||||||
const selectForCompareDropData = useMemo<SelectForCompareDropData>(
|
const selectForCompareDropData = useMemo<SelectForCompareDropData>(
|
||||||
() => ({
|
() => ({
|
||||||
@ -29,14 +22,6 @@ export const ImageComparisonDroppable = memo(() => {
|
|||||||
[firstImage?.image_name, secondImage?.image_name]
|
[firstImage?.image_name, secondImage?.image_name]
|
||||||
);
|
);
|
||||||
|
|
||||||
if (!imageViewer.isOpen) {
|
|
||||||
return (
|
|
||||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
|
||||||
<IAIDroppable data={setCurrentImageDropData} dropLabel={t('gallery.openInViewer')} />
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
<Flex position="absolute" top={0} right={0} bottom={0} left={0} gap={2} pointerEvents="none">
|
||||||
<IAIDroppable data={selectForCompareDropData} dropLabel={t('gallery.selectForCompare')} />
|
<IAIDroppable data={selectForCompareDropData} dropLabel={t('gallery.selectForCompare')} />
|
||||||
|
@ -1,19 +1,25 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useScopeOnFocus, useScopeOnMount } from 'common/hooks/interactionScopes';
|
||||||
import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
|
import { CompareToolbar } from 'features/gallery/components/ImageViewer/CompareToolbar';
|
||||||
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
import CurrentImagePreview from 'features/gallery/components/ImageViewer/CurrentImagePreview';
|
||||||
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
|
import { ImageComparison } from 'features/gallery/components/ImageViewer/ImageComparison';
|
||||||
|
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
|
||||||
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
|
import { ViewerToolbar } from 'features/gallery/components/ImageViewer/ViewerToolbar';
|
||||||
import { memo } from 'react';
|
import { memo, useRef } from 'react';
|
||||||
import { useMeasure } from 'react-use';
|
import { useMeasure } from 'react-use';
|
||||||
|
|
||||||
import { useImageViewer } from './useImageViewer';
|
|
||||||
|
|
||||||
export const ImageViewer = memo(() => {
|
export const ImageViewer = memo(() => {
|
||||||
const imageViewer = useImageViewer();
|
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
|
||||||
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
|
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useScopeOnFocus('imageViewer', ref);
|
||||||
|
useScopeOnMount('imageViewer');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex
|
<Flex
|
||||||
|
ref={ref}
|
||||||
|
tabIndex={-1}
|
||||||
layerStyle="first"
|
layerStyle="first"
|
||||||
borderRadius="base"
|
borderRadius="base"
|
||||||
position="absolute"
|
position="absolute"
|
||||||
@ -27,12 +33,13 @@ export const ImageViewer = memo(() => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
{imageViewer.isComparing && <CompareToolbar />}
|
{isComparing && <CompareToolbar />}
|
||||||
{!imageViewer.isComparing && <ViewerToolbar />}
|
{!isComparing && <ViewerToolbar />}
|
||||||
<Box ref={containerRef} w="full" h="full">
|
<Box ref={containerRef} w="full" h="full">
|
||||||
{!imageViewer.isComparing && <CurrentImagePreview />}
|
{!isComparing && <CurrentImagePreview />}
|
||||||
{imageViewer.isComparing && <ImageComparison containerDims={containerDims} />}
|
{isComparing && <ImageComparison containerDims={containerDims} />}
|
||||||
</Box>
|
</Box>
|
||||||
|
<ImageComparisonDroppable />
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
@ -2,30 +2,60 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|||||||
import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||||
import { useCallback } from 'react';
|
import { useCallback } from 'react';
|
||||||
|
|
||||||
|
export const useIsImageViewerOpen = () => {
|
||||||
|
const isOpen = useAppSelector((s) => {
|
||||||
|
const tab = s.ui.activeTab;
|
||||||
|
const workflowsMode = s.workflow.mode;
|
||||||
|
if (tab === 'models' || tab === 'queue') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tab === 'workflows' && workflowsMode === 'edit') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (tab === 'workflows' && workflowsMode === 'view') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (tab === 'upscaling') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return s.gallery.isImageViewerOpen;
|
||||||
|
});
|
||||||
|
return isOpen;
|
||||||
|
};
|
||||||
|
|
||||||
export const useImageViewer = () => {
|
export const useImageViewer = () => {
|
||||||
const dispatch = useAppDispatch();
|
const dispatch = useAppDispatch();
|
||||||
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
|
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
|
||||||
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
|
const isNaturallyOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
|
||||||
|
const isForcedOpen = useAppSelector(
|
||||||
|
(s) => s.ui.activeTab === 'upscaling' || (s.ui.activeTab === 'workflows' && s.workflow.mode === 'view')
|
||||||
|
);
|
||||||
|
|
||||||
const onClose = useCallback(() => {
|
const onClose = useCallback(() => {
|
||||||
if (isComparing && isOpen) {
|
if (isForcedOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isComparing && isNaturallyOpen) {
|
||||||
dispatch(imageToCompareChanged(null));
|
dispatch(imageToCompareChanged(null));
|
||||||
} else {
|
} else {
|
||||||
dispatch(isImageViewerOpenChanged(false));
|
dispatch(isImageViewerOpenChanged(false));
|
||||||
}
|
}
|
||||||
}, [dispatch, isComparing, isOpen]);
|
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
|
||||||
|
|
||||||
const onOpen = useCallback(() => {
|
const onOpen = useCallback(() => {
|
||||||
dispatch(isImageViewerOpenChanged(true));
|
dispatch(isImageViewerOpenChanged(true));
|
||||||
}, [dispatch]);
|
}, [dispatch]);
|
||||||
|
|
||||||
const onToggle = useCallback(() => {
|
const onToggle = useCallback(() => {
|
||||||
if (isComparing && isOpen) {
|
if (isForcedOpen) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (isComparing && isNaturallyOpen) {
|
||||||
dispatch(imageToCompareChanged(null));
|
dispatch(imageToCompareChanged(null));
|
||||||
} else {
|
} else {
|
||||||
dispatch(isImageViewerOpenChanged(!isOpen));
|
dispatch(isImageViewerOpenChanged(!isNaturallyOpen));
|
||||||
}
|
}
|
||||||
}, [dispatch, isComparing, isOpen]);
|
}, [dispatch, isComparing, isForcedOpen, isNaturallyOpen]);
|
||||||
|
|
||||||
return { isOpen, onOpen, onClose, onToggle, isComparing };
|
return { isOpen: isNaturallyOpen || isForcedOpen, onOpen, onClose, onToggle, isComparing };
|
||||||
};
|
};
|
||||||
|
@ -1,19 +1,35 @@
|
|||||||
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||||
|
import { $activeScopes } from 'common/hooks/interactionScopes';
|
||||||
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
import { useGalleryNavigation } from 'features/gallery/hooks/useGalleryNavigation';
|
||||||
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
|
import { useGalleryPagination } from 'features/gallery/hooks/useGalleryPagination';
|
||||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||||
|
import { $isGalleryPanelOpen } from 'features/ui/store/uiSlice';
|
||||||
|
import { computed } from 'nanostores';
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||||
|
|
||||||
|
const $leftRightHotkeysEnabled = computed($activeScopes, (activeScopes) => {
|
||||||
|
// The left and right hotkeys can be used when the gallery is focused and the canvas is not focused, OR when the image viewer is focused.
|
||||||
|
return (!activeScopes.has('staging-area') && !activeScopes.has('canvas')) || activeScopes.has('imageViewer');
|
||||||
|
});
|
||||||
|
|
||||||
|
const $upDownHotkeysEnabled = computed([$activeScopes, $isGalleryPanelOpen], (activeScopes, isGalleryPanelOpen) => {
|
||||||
|
// The up and down hotkeys can be used when the gallery is focused and the canvas is not focused, and the gallery panel is open.
|
||||||
|
return !activeScopes.has('staging-area') && !activeScopes.has('canvas') && isGalleryPanelOpen;
|
||||||
|
});
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers gallery hotkeys. This hook is a singleton.
|
* Registers gallery hotkeys. This hook is a singleton.
|
||||||
*/
|
*/
|
||||||
export const useGalleryHotkeys = () => {
|
export const useGalleryHotkeys = () => {
|
||||||
const isStaging = useAppSelector((s) => s.canvasV2.session.isStaging);
|
useAssertSingleton('useGalleryHotkeys');
|
||||||
|
|
||||||
const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
|
const { goNext, goPrev, isNextEnabled, isPrevEnabled } = useGalleryPagination();
|
||||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
||||||
const queryResult = useListImagesQuery(queryArgs);
|
const queryResult = useListImagesQuery(queryArgs);
|
||||||
|
const leftRightHotkeysEnabled = useStore($leftRightHotkeysEnabled);
|
||||||
|
const upDownHotkeysEnabled = useStore($upDownHotkeysEnabled);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
handleLeftImage,
|
handleLeftImage,
|
||||||
@ -35,15 +51,13 @@ export const useGalleryHotkeys = () => {
|
|||||||
}
|
}
|
||||||
handleLeftImage(e.altKey);
|
handleLeftImage(e.altKey);
|
||||||
},
|
},
|
||||||
[handleLeftImage, isOnFirstImageOfView, goPrev, isPrevEnabled, queryResult.isFetching]
|
{ preventDefault: true, enabled: leftRightHotkeysEnabled },
|
||||||
|
[handleLeftImage, isOnFirstImageOfView, goPrev, isPrevEnabled, queryResult.isFetching, leftRightHotkeysEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
['right', 'alt+right'],
|
['right', 'alt+right'],
|
||||||
(e) => {
|
(e) => {
|
||||||
if (isStaging) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) {
|
if (isOnLastImageOfView && isNextEnabled && !queryResult.isFetching) {
|
||||||
goNext(e.altKey ? 'alt+arrow' : 'arrow');
|
goNext(e.altKey ? 'alt+arrow' : 'arrow');
|
||||||
return;
|
return;
|
||||||
@ -52,38 +66,33 @@ export const useGalleryHotkeys = () => {
|
|||||||
handleRightImage(e.altKey);
|
handleRightImage(e.altKey);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[isStaging, isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage]
|
{ preventDefault: true, enabled: leftRightHotkeysEnabled },
|
||||||
|
[isOnLastImageOfView, goNext, isNextEnabled, queryResult.isFetching, handleRightImage, leftRightHotkeysEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
['up', 'alt+up'],
|
['up', 'alt+up'],
|
||||||
(e) => {
|
(e) => {
|
||||||
if (isStaging) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isOnFirstRow && isPrevEnabled && !queryResult.isFetching) {
|
if (isOnFirstRow && isPrevEnabled && !queryResult.isFetching) {
|
||||||
goPrev(e.altKey ? 'alt+arrow' : 'arrow');
|
goPrev(e.altKey ? 'alt+arrow' : 'arrow');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleUpImage(e.altKey);
|
handleUpImage(e.altKey);
|
||||||
},
|
},
|
||||||
{ preventDefault: true },
|
{ preventDefault: true, enabled: upDownHotkeysEnabled },
|
||||||
[isStaging, handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching]
|
[handleUpImage, isOnFirstRow, goPrev, isPrevEnabled, queryResult.isFetching, upDownHotkeysEnabled]
|
||||||
);
|
);
|
||||||
|
|
||||||
useHotkeys(
|
useHotkeys(
|
||||||
['down', 'alt+down'],
|
['down', 'alt+down'],
|
||||||
(e) => {
|
(e) => {
|
||||||
if (isStaging) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (isOnLastRow && isNextEnabled && !queryResult.isFetching) {
|
if (isOnLastRow && isNextEnabled && !queryResult.isFetching) {
|
||||||
goNext(e.altKey ? 'alt+arrow' : 'arrow');
|
goNext(e.altKey ? 'alt+arrow' : 'arrow');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
handleDownImage(e.altKey);
|
handleDownImage(e.altKey);
|
||||||
},
|
},
|
||||||
{ preventDefault: true },
|
{ preventDefault: true, enabled: upDownHotkeysEnabled },
|
||||||
[isStaging, isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage]
|
[isOnLastRow, goNext, isNextEnabled, queryResult.isFetching, handleDownImage, upDownHotkeysEnabled]
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,9 +5,6 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
|||||||
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
import TopPanel from 'features/nodes/components/flow/panels/TopPanel/TopPanel';
|
||||||
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
import { LoadWorkflowFromGraphModal } from 'features/workflowLibrary/components/LoadWorkflowFromGraphModal/LoadWorkflowFromGraphModal';
|
||||||
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
import { SaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/SaveWorkflowAsDialog';
|
||||||
import type { AnimationProps } from 'framer-motion';
|
|
||||||
import { AnimatePresence, motion } from 'framer-motion';
|
|
||||||
import type { CSSProperties } from 'react';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
import { MdDeviceHub } from 'react-icons/md';
|
import { MdDeviceHub } from 'react-icons/md';
|
||||||
@ -18,28 +15,6 @@ import { Flow } from './flow/Flow';
|
|||||||
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
|
import BottomLeftPanel from './flow/panels/BottomLeftPanel/BottomLeftPanel';
|
||||||
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
import MinimapPanel from './flow/panels/MinimapPanel/MinimapPanel';
|
||||||
|
|
||||||
const isReadyMotionStyles: CSSProperties = {
|
|
||||||
position: 'relative',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
};
|
|
||||||
const notIsReadyMotionStyles: CSSProperties = {
|
|
||||||
position: 'absolute',
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
};
|
|
||||||
const initial: AnimationProps['initial'] = {
|
|
||||||
opacity: 0,
|
|
||||||
};
|
|
||||||
const animate: AnimationProps['animate'] = {
|
|
||||||
opacity: 1,
|
|
||||||
transition: { duration: 0.2 },
|
|
||||||
};
|
|
||||||
const exit: AnimationProps['exit'] = {
|
|
||||||
opacity: 0,
|
|
||||||
transition: { duration: 0.2 },
|
|
||||||
};
|
|
||||||
|
|
||||||
const NodeEditor = () => {
|
const NodeEditor = () => {
|
||||||
const { data, isLoading } = useGetOpenAPISchemaQuery();
|
const { data, isLoading } = useGetOpenAPISchemaQuery();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
@ -53,9 +28,8 @@ const NodeEditor = () => {
|
|||||||
alignItems="center"
|
alignItems="center"
|
||||||
justifyContent="center"
|
justifyContent="center"
|
||||||
>
|
>
|
||||||
<AnimatePresence>
|
|
||||||
{data && (
|
{data && (
|
||||||
<motion.div initial={initial} animate={animate} exit={exit} style={isReadyMotionStyles}>
|
<>
|
||||||
<Flow />
|
<Flow />
|
||||||
<AddNodePopover />
|
<AddNodePopover />
|
||||||
<TopPanel />
|
<TopPanel />
|
||||||
@ -63,27 +37,9 @@ const NodeEditor = () => {
|
|||||||
<MinimapPanel />
|
<MinimapPanel />
|
||||||
<SaveWorkflowAsDialog />
|
<SaveWorkflowAsDialog />
|
||||||
<LoadWorkflowFromGraphModal />
|
<LoadWorkflowFromGraphModal />
|
||||||
</motion.div>
|
</>
|
||||||
)}
|
)}
|
||||||
</AnimatePresence>
|
{isLoading && <IAINoContentFallback label={t('nodes.loadingNodes')} icon={MdDeviceHub} />}
|
||||||
<AnimatePresence>
|
|
||||||
{isLoading && (
|
|
||||||
<motion.div initial={initial} animate={animate} exit={exit} style={notIsReadyMotionStyles}>
|
|
||||||
<Flex
|
|
||||||
layerStyle="first"
|
|
||||||
position="relative"
|
|
||||||
width="full"
|
|
||||||
height="full"
|
|
||||||
borderRadius="base"
|
|
||||||
alignItems="center"
|
|
||||||
justifyContent="center"
|
|
||||||
pointerEvents="none"
|
|
||||||
>
|
|
||||||
<IAINoContentFallback label={t('nodes.loadingNodes')} icon={MdDeviceHub} />
|
|
||||||
</Flex>
|
|
||||||
</motion.div>
|
|
||||||
)}
|
|
||||||
</AnimatePresence>
|
|
||||||
</Flex>
|
</Flex>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,7 @@ import { Combobox, Flex, Popover, PopoverAnchor, PopoverBody, PopoverContent } f
|
|||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppStore } from 'app/store/storeHooks';
|
||||||
import type { SelectInstance } from 'chakra-react-select';
|
import type { SelectInstance } from 'chakra-react-select';
|
||||||
|
import { INTERACTION_SCOPES } from 'common/hooks/interactionScopes';
|
||||||
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
import { useBuildNode } from 'features/nodes/hooks/useBuildNode';
|
||||||
import {
|
import {
|
||||||
$cursorPos,
|
$cursorPos,
|
||||||
@ -67,6 +68,7 @@ const AddNodePopover = () => {
|
|||||||
const pendingConnection = useStore($pendingConnection);
|
const pendingConnection = useStore($pendingConnection);
|
||||||
const isOpen = useStore($isAddNodePopoverOpen);
|
const isOpen = useStore($isAddNodePopoverOpen);
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
|
const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
|
||||||
|
|
||||||
const filteredTemplates = useMemo(() => {
|
const filteredTemplates = useMemo(() => {
|
||||||
// If we have a connection in progress, we need to filter the node choices
|
// If we have a connection in progress, we need to filter the node choices
|
||||||
@ -214,14 +216,7 @@ const AddNodePopover = () => {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleHotkeyClose: HotkeyCallback = useCallback(() => {
|
useHotkeys(['shift+a', 'space'], handleHotkeyOpen, { enabled: isWorkflowsActive }, [isWorkflowsActive]);
|
||||||
if ($isAddNodePopoverOpen.get()) {
|
|
||||||
closeAddNodePopover();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
useHotkeys(['shift+a', 'space'], handleHotkeyOpen);
|
|
||||||
useHotkeys(['escape'], handleHotkeyClose, { enableOnFormTags: ['TEXTAREA'] });
|
|
||||||
|
|
||||||
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
|
const noOptionsMessage = useCallback(() => t('nodes.noMatchingNodes'), [t]);
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
import { useGlobalMenuClose, useToken } from '@invoke-ai/ui-library';
|
||||||
import { useStore } from '@nanostores/react';
|
import { useStore } from '@nanostores/react';
|
||||||
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
import { useAppDispatch, useAppSelector, useAppStore } from 'app/store/storeHooks';
|
||||||
|
import { INTERACTION_SCOPES, useScopeImperativeApi } from 'common/hooks/interactionScopes';
|
||||||
import { useConnection } from 'features/nodes/hooks/useConnection';
|
import { useConnection } from 'features/nodes/hooks/useConnection';
|
||||||
import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste';
|
import { useCopyPaste } from 'features/nodes/hooks/useCopyPaste';
|
||||||
import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
|
import { useSyncExecutionState } from 'features/nodes/hooks/useExecutionState';
|
||||||
@ -79,16 +80,13 @@ export const Flow = memo(() => {
|
|||||||
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
const cancelConnection = useReactFlowStore(selectCancelConnection);
|
||||||
const updateNodeInternals = useUpdateNodeInternals();
|
const updateNodeInternals = useUpdateNodeInternals();
|
||||||
const store = useAppStore();
|
const store = useAppStore();
|
||||||
|
const isWorkflowsActive = useStore(INTERACTION_SCOPES.workflows.$isActive);
|
||||||
|
const workflowsScopeApi = useScopeImperativeApi('workflows');
|
||||||
|
|
||||||
useWorkflowWatcher();
|
useWorkflowWatcher();
|
||||||
useSyncExecutionState();
|
useSyncExecutionState();
|
||||||
const [borderRadius] = useToken('radii', ['base']);
|
const [borderRadius] = useToken('radii', ['base']);
|
||||||
|
const flowStyles = useMemo<CSSProperties>(() => ({ borderRadius }), [borderRadius]);
|
||||||
const flowStyles = useMemo<CSSProperties>(
|
|
||||||
() => ({
|
|
||||||
borderRadius,
|
|
||||||
}),
|
|
||||||
[borderRadius]
|
|
||||||
);
|
|
||||||
|
|
||||||
const onNodesChange: OnNodesChange = useCallback(
|
const onNodesChange: OnNodesChange = useCallback(
|
||||||
(nodeChanges) => {
|
(nodeChanges) => {
|
||||||
@ -121,7 +119,8 @@ export const Flow = memo(() => {
|
|||||||
const { onCloseGlobal } = useGlobalMenuClose();
|
const { onCloseGlobal } = useGlobalMenuClose();
|
||||||
const handlePaneClick = useCallback(() => {
|
const handlePaneClick = useCallback(() => {
|
||||||
onCloseGlobal();
|
onCloseGlobal();
|
||||||
}, [onCloseGlobal]);
|
workflowsScopeApi.add();
|
||||||
|
}, [onCloseGlobal, workflowsScopeApi]);
|
||||||
|
|
||||||
const onInit: OnInit = useCallback((flow) => {
|
const onInit: OnInit = useCallback((flow) => {
|
||||||
$flow.set(flow);
|
$flow.set(flow);
|
||||||
@ -237,7 +236,7 @@ export const Flow = memo(() => {
|
|||||||
},
|
},
|
||||||
[dispatch, store]
|
[dispatch, store]
|
||||||
);
|
);
|
||||||
useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey);
|
useHotkeys(['Ctrl+a', 'Meta+a'], onSelectAllHotkey, { enabled: isWorkflowsActive }, [isWorkflowsActive]);
|
||||||
|
|
||||||
const onPasteHotkey = useCallback(
|
const onPasteHotkey = useCallback(
|
||||||
(e: KeyboardEvent) => {
|
(e: KeyboardEvent) => {
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
import InvocationCacheStatus from './InvocationCacheStatus';
|
import InvocationCacheStatus from './InvocationCacheStatus';
|
||||||
@ -9,9 +11,18 @@ import QueueTabQueueControls from './QueueTabQueueControls';
|
|||||||
|
|
||||||
const QueueTabContent = () => {
|
const QueueTabContent = () => {
|
||||||
const isInvocationCacheEnabled = useFeatureStatus('invocationCache');
|
const isInvocationCacheEnabled = useFeatureStatus('invocationCache');
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex borderRadius="base" w="full" h="full" flexDir="column" gap={2}>
|
<Flex
|
||||||
|
display={activeTabName === 'queue' ? undefined : 'none'}
|
||||||
|
hidden={activeTabName !== 'queue'}
|
||||||
|
borderRadius="base"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
flexDir="column"
|
||||||
|
gap={2}
|
||||||
|
>
|
||||||
<Flex gap={2} w="full">
|
<Flex gap={2} w="full">
|
||||||
<QueueTabQueueControls />
|
<QueueTabQueueControls />
|
||||||
<QueueStatus />
|
<QueueStatus />
|
||||||
|
181
invokeai/frontend/web/src/features/ui/components/AppContent.tsx
Normal file
181
invokeai/frontend/web/src/features/ui/components/AppContent.tsx
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||||
|
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
|
import GalleryPanelContent from 'features/gallery/components/GalleryPanelContent';
|
||||||
|
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
||||||
|
import { useIsImageViewerOpen } from 'features/gallery/components/ImageViewer/useImageViewer';
|
||||||
|
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
||||||
|
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
||||||
|
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
||||||
|
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
||||||
|
import { TabMountGate } from 'features/ui/components/TabMountGate';
|
||||||
|
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
||||||
|
import NodesTab from 'features/ui/components/tabs/NodesTab';
|
||||||
|
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
||||||
|
import { TabVisibilityGate } from 'features/ui/components/TabVisibilityGate';
|
||||||
|
import { VerticalNavBar } from 'features/ui/components/VerticalNavBar';
|
||||||
|
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||||
|
import { usePanel } from 'features/ui/hooks/usePanel';
|
||||||
|
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
||||||
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import { $isGalleryPanelOpen, $isParametersPanelOpen } from 'features/ui/store/uiSlice';
|
||||||
|
import type { CSSProperties } from 'react';
|
||||||
|
import { memo, useMemo, useRef } from 'react';
|
||||||
|
import { useHotkeys } from 'react-hotkeys-hook';
|
||||||
|
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
||||||
|
import { Panel, PanelGroup } from 'react-resizable-panels';
|
||||||
|
|
||||||
|
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
||||||
|
import ResizeHandle from './tabs/ResizeHandle';
|
||||||
|
|
||||||
|
const TABS_WITH_GALLERY_PANEL: InvokeTabName[] = ['generation', 'upscaling', 'workflows'] as const;
|
||||||
|
const TABS_WITH_OPTIONS_PANEL: InvokeTabName[] = ['generation', 'upscaling', 'workflows'] as const;
|
||||||
|
|
||||||
|
const panelStyles: CSSProperties = { position: 'relative', height: '100%', width: '100%' };
|
||||||
|
const GALLERY_MIN_SIZE_PX = 310;
|
||||||
|
const GALLERY_MIN_SIZE_PCT = 20;
|
||||||
|
const OPTIONS_PANEL_MIN_SIZE_PX = 430;
|
||||||
|
const OPTIONS_PANEL_MIN_SIZE_PCT = 20;
|
||||||
|
|
||||||
|
export const onGalleryPanelCollapse = (isCollapsed: boolean) => $isGalleryPanelOpen.set(!isCollapsed);
|
||||||
|
export const onParametersPanelCollapse = (isCollapsed: boolean) => $isParametersPanelOpen.set(!isCollapsed);
|
||||||
|
|
||||||
|
export const AppContent = memo(() => {
|
||||||
|
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||||
|
const isImageViewerOpen = useIsImageViewerOpen();
|
||||||
|
const shouldShowGalleryPanel = useAppSelector((s) => TABS_WITH_GALLERY_PANEL.includes(s.ui.activeTab));
|
||||||
|
const shouldShowOptionsPanel = useAppSelector((s) => TABS_WITH_OPTIONS_PANEL.includes(s.ui.activeTab));
|
||||||
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
|
useScopeOnFocus('gallery', ref);
|
||||||
|
|
||||||
|
const optionsPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
||||||
|
() => ({
|
||||||
|
id: 'options-panel',
|
||||||
|
unit: 'pixels',
|
||||||
|
minSize: OPTIONS_PANEL_MIN_SIZE_PX,
|
||||||
|
defaultSize: OPTIONS_PANEL_MIN_SIZE_PCT,
|
||||||
|
panelGroupRef,
|
||||||
|
panelGroupDirection: 'horizontal',
|
||||||
|
onCollapse: onParametersPanelCollapse,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const galleryPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
||||||
|
() => ({
|
||||||
|
id: 'gallery-panel',
|
||||||
|
unit: 'pixels',
|
||||||
|
minSize: GALLERY_MIN_SIZE_PX,
|
||||||
|
defaultSize: GALLERY_MIN_SIZE_PCT,
|
||||||
|
panelGroupRef,
|
||||||
|
panelGroupDirection: 'horizontal',
|
||||||
|
onCollapse: onGalleryPanelCollapse,
|
||||||
|
}),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
|
const panelStorage = usePanelStorage();
|
||||||
|
|
||||||
|
const optionsPanel = usePanel(optionsPanelUsePanelOptions);
|
||||||
|
|
||||||
|
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
|
||||||
|
|
||||||
|
useHotkeys('g', galleryPanel.toggle, [galleryPanel.toggle]);
|
||||||
|
useHotkeys(['t', 'o'], optionsPanel.toggle, [optionsPanel.toggle]);
|
||||||
|
useHotkeys(
|
||||||
|
'shift+r',
|
||||||
|
() => {
|
||||||
|
optionsPanel.reset();
|
||||||
|
galleryPanel.reset();
|
||||||
|
},
|
||||||
|
[optionsPanel.reset, galleryPanel.reset]
|
||||||
|
);
|
||||||
|
useHotkeys(
|
||||||
|
'f',
|
||||||
|
() => {
|
||||||
|
if (optionsPanel.isCollapsed || galleryPanel.isCollapsed) {
|
||||||
|
optionsPanel.expand();
|
||||||
|
galleryPanel.expand();
|
||||||
|
} else {
|
||||||
|
optionsPanel.collapse();
|
||||||
|
galleryPanel.collapse();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[
|
||||||
|
optionsPanel.isCollapsed,
|
||||||
|
galleryPanel.isCollapsed,
|
||||||
|
optionsPanel.expand,
|
||||||
|
galleryPanel.expand,
|
||||||
|
optionsPanel.collapse,
|
||||||
|
galleryPanel.collapse,
|
||||||
|
]
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex ref={ref} id="invoke-app-tabs" w="full" h="full" gap={4} p={4}>
|
||||||
|
<VerticalNavBar />
|
||||||
|
<Flex position="relative" w="full" h="full" gap={4}>
|
||||||
|
<PanelGroup
|
||||||
|
ref={panelGroupRef}
|
||||||
|
id="app-panel-group"
|
||||||
|
autoSaveId="app"
|
||||||
|
direction="horizontal"
|
||||||
|
style={panelStyles}
|
||||||
|
storage={panelStorage}
|
||||||
|
>
|
||||||
|
<Panel order={0} collapsible style={panelStyles} {...optionsPanel.panelProps}>
|
||||||
|
<TabMountGate tab="generation">
|
||||||
|
<TabVisibilityGate tab="generation">
|
||||||
|
<ParametersPanelTextToImage />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
<TabMountGate tab="upscaling">
|
||||||
|
<TabVisibilityGate tab="upscaling">
|
||||||
|
<ParametersPanelUpscale />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
<TabMountGate tab="workflows">
|
||||||
|
<TabVisibilityGate tab="workflows">
|
||||||
|
<NodeEditorPanelGroup />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
</Panel>
|
||||||
|
<ResizeHandle id="options-main-handle" orientation="vertical" {...optionsPanel.resizeHandleProps} />
|
||||||
|
<Panel id="main-panel" order={1} minSize={20} style={panelStyles}>
|
||||||
|
<TabMountGate tab="generation">
|
||||||
|
<TabVisibilityGate tab="generation">
|
||||||
|
<CanvasEditor />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
{/* upscaling tab has no content of its own - uses image viewer only */}
|
||||||
|
<TabMountGate tab="workflows">
|
||||||
|
<TabVisibilityGate tab="workflows">
|
||||||
|
<NodesTab />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
{isImageViewerOpen && <ImageViewer />}
|
||||||
|
</Panel>
|
||||||
|
<ResizeHandle id="main-gallery-handle" orientation="vertical" {...galleryPanel.resizeHandleProps} />
|
||||||
|
<Panel order={2} style={panelStyles} collapsible {...galleryPanel.panelProps}>
|
||||||
|
<GalleryPanelContent />
|
||||||
|
</Panel>
|
||||||
|
</PanelGroup>
|
||||||
|
{shouldShowOptionsPanel && <FloatingParametersPanelButtons panelApi={optionsPanel} />}
|
||||||
|
{shouldShowGalleryPanel && <FloatingGalleryButton panelApi={galleryPanel} />}
|
||||||
|
<TabMountGate tab="models">
|
||||||
|
<TabVisibilityGate tab="models">
|
||||||
|
<ModelManagerTab />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
<TabMountGate tab="models">
|
||||||
|
<TabVisibilityGate tab="queue">
|
||||||
|
<QueueTab />
|
||||||
|
</TabVisibilityGate>
|
||||||
|
</TabMountGate>
|
||||||
|
</Flex>
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
AppContent.displayName = 'AppContent';
|
@ -1,303 +0,0 @@
|
|||||||
import { Flex, IconButton, Spacer, Tab, TabList, TabPanel, TabPanels, Tabs, Tooltip } from '@invoke-ai/ui-library';
|
|
||||||
import { useStore } from '@nanostores/react';
|
|
||||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
|
||||||
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
|
||||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
|
||||||
import ImageGalleryContent from 'features/gallery/components/ImageGalleryContent';
|
|
||||||
import NodeEditorPanelGroup from 'features/nodes/components/sidePanel/NodeEditorPanelGroup';
|
|
||||||
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
|
||||||
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
|
||||||
import StatusIndicator from 'features/system/components/StatusIndicator';
|
|
||||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
|
||||||
import FloatingGalleryButton from 'features/ui/components/FloatingGalleryButton';
|
|
||||||
import FloatingParametersPanelButtons from 'features/ui/components/FloatingParametersPanelButtons';
|
|
||||||
import ParametersPanelTextToImage from 'features/ui/components/ParametersPanels/ParametersPanelTextToImage';
|
|
||||||
import ModelManagerTab from 'features/ui/components/tabs/ModelManagerTab';
|
|
||||||
import NodesTab from 'features/ui/components/tabs/NodesTab';
|
|
||||||
import QueueTab from 'features/ui/components/tabs/QueueTab';
|
|
||||||
import TextToImageTab from 'features/ui/components/tabs/TextToImageTab';
|
|
||||||
import type { UsePanelOptions } from 'features/ui/hooks/usePanel';
|
|
||||||
import { usePanel } from 'features/ui/hooks/usePanel';
|
|
||||||
import { usePanelStorage } from 'features/ui/hooks/usePanelStorage';
|
|
||||||
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
|
||||||
import { TAB_NUMBER_MAP } from 'features/ui/store/tabMap';
|
|
||||||
import { activeTabIndexSelector, activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
|
||||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
|
||||||
import type { CSSProperties, MouseEvent, ReactElement, ReactNode } from 'react';
|
|
||||||
import { memo, useCallback, useMemo, useRef } from 'react';
|
|
||||||
import { useHotkeys } from 'react-hotkeys-hook';
|
|
||||||
import { useTranslation } from 'react-i18next';
|
|
||||||
import { MdZoomOutMap } from 'react-icons/md';
|
|
||||||
import { PiFlowArrowBold } from 'react-icons/pi';
|
|
||||||
import { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri';
|
|
||||||
import type { ImperativePanelGroupHandle } from 'react-resizable-panels';
|
|
||||||
import { Panel, PanelGroup } from 'react-resizable-panels';
|
|
||||||
|
|
||||||
import ParametersPanelUpscale from './ParametersPanels/ParametersPanelUpscale';
|
|
||||||
import ResizeHandle from './tabs/ResizeHandle';
|
|
||||||
import UpscalingTab from './tabs/UpscalingTab';
|
|
||||||
|
|
||||||
type TabData = {
|
|
||||||
id: InvokeTabName;
|
|
||||||
translationKey: string;
|
|
||||||
icon: ReactElement;
|
|
||||||
content: ReactNode;
|
|
||||||
parametersPanel?: ReactNode;
|
|
||||||
};
|
|
||||||
|
|
||||||
const TAB_DATA: Record<InvokeTabName, TabData> = {
|
|
||||||
generation: {
|
|
||||||
id: 'generation',
|
|
||||||
translationKey: 'ui.tabs.generation',
|
|
||||||
icon: <RiInputMethodLine />,
|
|
||||||
content: <TextToImageTab />,
|
|
||||||
parametersPanel: <ParametersPanelTextToImage />,
|
|
||||||
},
|
|
||||||
upscaling: {
|
|
||||||
id: 'upscaling',
|
|
||||||
translationKey: 'ui.tabs.upscaling',
|
|
||||||
icon: <MdZoomOutMap />,
|
|
||||||
content: <UpscalingTab />,
|
|
||||||
parametersPanel: <ParametersPanelUpscale />,
|
|
||||||
},
|
|
||||||
workflows: {
|
|
||||||
id: 'workflows',
|
|
||||||
translationKey: 'ui.tabs.workflows',
|
|
||||||
icon: <PiFlowArrowBold />,
|
|
||||||
content: <NodesTab />,
|
|
||||||
parametersPanel: <NodeEditorPanelGroup />,
|
|
||||||
},
|
|
||||||
models: {
|
|
||||||
id: 'models',
|
|
||||||
translationKey: 'ui.tabs.models',
|
|
||||||
icon: <RiBox2Line />,
|
|
||||||
content: <ModelManagerTab />,
|
|
||||||
},
|
|
||||||
queue: {
|
|
||||||
id: 'queue',
|
|
||||||
translationKey: 'ui.tabs.queue',
|
|
||||||
icon: <RiPlayList2Fill />,
|
|
||||||
content: <QueueTab />,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
const enabledTabsSelector = createMemoizedSelector(selectConfigSlice, (config) =>
|
|
||||||
TAB_NUMBER_MAP.map((tabName) => TAB_DATA[tabName]).filter((tab) => !config.disabledTabs.includes(tab.id))
|
|
||||||
);
|
|
||||||
|
|
||||||
const NO_GALLERY_PANEL_TABS: InvokeTabName[] = ['models', 'queue'];
|
|
||||||
const panelStyles: CSSProperties = { height: '100%', width: '100%' };
|
|
||||||
const GALLERY_MIN_SIZE_PX = 310;
|
|
||||||
const GALLERY_MIN_SIZE_PCT = 20;
|
|
||||||
const OPTIONS_PANEL_MIN_SIZE_PX = 430;
|
|
||||||
const OPTIONS_PANEL_MIN_SIZE_PCT = 20;
|
|
||||||
|
|
||||||
const appPanelGroupId = 'app-panel-group';
|
|
||||||
|
|
||||||
const InvokeTabs = () => {
|
|
||||||
const activeTabIndex = useAppSelector(activeTabIndexSelector);
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
const enabledTabs = useAppSelector(enabledTabsSelector);
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const dispatch = useAppDispatch();
|
|
||||||
const customNavComponent = useStore($customNavComponent);
|
|
||||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
|
||||||
const handleClickTab = useCallback((e: MouseEvent<HTMLElement>) => {
|
|
||||||
if (e.target instanceof HTMLElement) {
|
|
||||||
e.target.blur();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
const shouldShowGalleryPanel = useMemo(() => !NO_GALLERY_PANEL_TABS.includes(activeTabName), [activeTabName]);
|
|
||||||
|
|
||||||
const tabs = useMemo(
|
|
||||||
() =>
|
|
||||||
enabledTabs.map((tab) => (
|
|
||||||
<Tooltip key={tab.id} label={t(tab.translationKey)} placement="end">
|
|
||||||
<Tab
|
|
||||||
as={IconButton}
|
|
||||||
p={0}
|
|
||||||
onClick={handleClickTab}
|
|
||||||
icon={tab.icon}
|
|
||||||
size="md"
|
|
||||||
fontSize="24px"
|
|
||||||
variant="appTab"
|
|
||||||
data-selected={activeTabName === tab.id}
|
|
||||||
aria-label={t(tab.translationKey)}
|
|
||||||
data-testid={t(tab.translationKey)}
|
|
||||||
/>
|
|
||||||
</Tooltip>
|
|
||||||
)),
|
|
||||||
[enabledTabs, t, handleClickTab, activeTabName]
|
|
||||||
);
|
|
||||||
|
|
||||||
const tabPanels = useMemo(
|
|
||||||
() => enabledTabs.map((tab) => <TabPanel key={tab.id}>{tab.content}</TabPanel>),
|
|
||||||
[enabledTabs]
|
|
||||||
);
|
|
||||||
|
|
||||||
const handleTabChange = useCallback(
|
|
||||||
(index: number) => {
|
|
||||||
const tab = enabledTabs[index];
|
|
||||||
if (!tab) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
dispatch(setActiveTab(tab.id));
|
|
||||||
},
|
|
||||||
[dispatch, enabledTabs]
|
|
||||||
);
|
|
||||||
|
|
||||||
const optionsPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
|
||||||
() => ({
|
|
||||||
unit: 'pixels',
|
|
||||||
minSize: OPTIONS_PANEL_MIN_SIZE_PX,
|
|
||||||
fallbackMinSizePct: OPTIONS_PANEL_MIN_SIZE_PCT,
|
|
||||||
panelGroupRef,
|
|
||||||
panelGroupDirection: 'horizontal',
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const galleryPanelUsePanelOptions = useMemo<UsePanelOptions>(
|
|
||||||
() => ({
|
|
||||||
unit: 'pixels',
|
|
||||||
minSize: GALLERY_MIN_SIZE_PX,
|
|
||||||
fallbackMinSizePct: GALLERY_MIN_SIZE_PCT,
|
|
||||||
panelGroupRef,
|
|
||||||
panelGroupDirection: 'horizontal',
|
|
||||||
}),
|
|
||||||
[]
|
|
||||||
);
|
|
||||||
|
|
||||||
const panelStorage = usePanelStorage();
|
|
||||||
|
|
||||||
const optionsPanel = usePanel(optionsPanelUsePanelOptions);
|
|
||||||
|
|
||||||
const galleryPanel = usePanel(galleryPanelUsePanelOptions);
|
|
||||||
|
|
||||||
useHotkeys('g', galleryPanel.toggle, [galleryPanel.toggle]);
|
|
||||||
useHotkeys(['t', 'o'], optionsPanel.toggle, [optionsPanel.toggle]);
|
|
||||||
useHotkeys(
|
|
||||||
'shift+r',
|
|
||||||
() => {
|
|
||||||
optionsPanel.reset();
|
|
||||||
galleryPanel.reset();
|
|
||||||
},
|
|
||||||
[optionsPanel.reset, galleryPanel.reset]
|
|
||||||
);
|
|
||||||
useHotkeys(
|
|
||||||
'f',
|
|
||||||
() => {
|
|
||||||
if (optionsPanel.isCollapsed || galleryPanel.isCollapsed) {
|
|
||||||
optionsPanel.expand();
|
|
||||||
galleryPanel.expand();
|
|
||||||
} else {
|
|
||||||
optionsPanel.collapse();
|
|
||||||
galleryPanel.collapse();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
[
|
|
||||||
optionsPanel.isCollapsed,
|
|
||||||
galleryPanel.isCollapsed,
|
|
||||||
optionsPanel.expand,
|
|
||||||
galleryPanel.expand,
|
|
||||||
optionsPanel.collapse,
|
|
||||||
galleryPanel.collapse,
|
|
||||||
]
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Tabs
|
|
||||||
id="invoke-app-tabs"
|
|
||||||
variant="appTabs"
|
|
||||||
defaultIndex={activeTabIndex}
|
|
||||||
index={activeTabIndex}
|
|
||||||
onChange={handleTabChange}
|
|
||||||
w="full"
|
|
||||||
h="full"
|
|
||||||
gap={4}
|
|
||||||
p={4}
|
|
||||||
isLazy
|
|
||||||
>
|
|
||||||
<Flex flexDir="column" alignItems="center" pt={4} pb={2} gap={4}>
|
|
||||||
<InvokeAILogoComponent />
|
|
||||||
<TabList gap={4} pt={6} h="full" flexDir="column">
|
|
||||||
{tabs}
|
|
||||||
</TabList>
|
|
||||||
<Spacer />
|
|
||||||
<StatusIndicator />
|
|
||||||
{customNavComponent ? customNavComponent : <SettingsMenu />}
|
|
||||||
</Flex>
|
|
||||||
<PanelGroup
|
|
||||||
ref={panelGroupRef}
|
|
||||||
id={appPanelGroupId}
|
|
||||||
autoSaveId="app"
|
|
||||||
direction="horizontal"
|
|
||||||
style={panelStyles}
|
|
||||||
storage={panelStorage}
|
|
||||||
>
|
|
||||||
{!!TAB_DATA[activeTabName].parametersPanel && (
|
|
||||||
<>
|
|
||||||
<Panel
|
|
||||||
id="options-panel"
|
|
||||||
ref={optionsPanel.ref}
|
|
||||||
order={0}
|
|
||||||
defaultSize={optionsPanel.minSize}
|
|
||||||
minSize={optionsPanel.minSize}
|
|
||||||
onCollapse={optionsPanel.onCollapse}
|
|
||||||
onExpand={optionsPanel.onExpand}
|
|
||||||
collapsible
|
|
||||||
>
|
|
||||||
{TAB_DATA[activeTabName].parametersPanel}
|
|
||||||
</Panel>
|
|
||||||
<ResizeHandle
|
|
||||||
id="options-main-handle"
|
|
||||||
onDoubleClick={optionsPanel.onDoubleClickHandle}
|
|
||||||
orientation="vertical"
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
<Panel id="main-panel" order={1} minSize={20}>
|
|
||||||
<TabPanels w="full" h="full">
|
|
||||||
{tabPanels}
|
|
||||||
</TabPanels>
|
|
||||||
</Panel>
|
|
||||||
{shouldShowGalleryPanel && (
|
|
||||||
<>
|
|
||||||
<ResizeHandle
|
|
||||||
id="main-gallery-handle"
|
|
||||||
orientation="vertical"
|
|
||||||
onDoubleClick={galleryPanel.onDoubleClickHandle}
|
|
||||||
/>
|
|
||||||
<Panel
|
|
||||||
id="gallery-panel"
|
|
||||||
ref={galleryPanel.ref}
|
|
||||||
order={2}
|
|
||||||
defaultSize={galleryPanel.minSize}
|
|
||||||
minSize={galleryPanel.minSize}
|
|
||||||
onCollapse={galleryPanel.onCollapse}
|
|
||||||
onExpand={galleryPanel.onExpand}
|
|
||||||
collapsible
|
|
||||||
>
|
|
||||||
<ImageGalleryContent />
|
|
||||||
</Panel>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</PanelGroup>
|
|
||||||
{!!TAB_DATA[activeTabName].parametersPanel && <FloatingParametersPanelButtons panelApi={optionsPanel} />}
|
|
||||||
{shouldShowGalleryPanel && <FloatingGalleryButton panelApi={galleryPanel} />}
|
|
||||||
</Tabs>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default memo(InvokeTabs);
|
|
||||||
|
|
||||||
const ParametersPanelComponent = memo(() => {
|
|
||||||
const activeTabName = useAppSelector(activeTabNameSelector);
|
|
||||||
|
|
||||||
if (activeTabName === 'workflows') {
|
|
||||||
return <NodeEditorPanelGroup />;
|
|
||||||
} else {
|
|
||||||
return <ParametersPanelTextToImage />;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
ParametersPanelComponent.displayName = 'ParametersPanelComponent';
|
|
@ -0,0 +1,32 @@
|
|||||||
|
import { IconButton, Tooltip } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||||
|
import { memo, type ReactElement, useCallback } from 'react';
|
||||||
|
|
||||||
|
export const TabButton = memo(({ tab, icon, label }: { tab: InvokeTabName; icon: ReactElement; label: string }) => {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
const onClick = useCallback(() => {
|
||||||
|
dispatch(setActiveTab(tab));
|
||||||
|
}, [dispatch, tab]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Tooltip label={label} placement="end">
|
||||||
|
<IconButton
|
||||||
|
p={0}
|
||||||
|
onClick={onClick}
|
||||||
|
icon={icon}
|
||||||
|
size="md"
|
||||||
|
fontSize="24px"
|
||||||
|
variant="appTab"
|
||||||
|
data-selected={activeTabName === tab}
|
||||||
|
aria-label={label}
|
||||||
|
data-testid={label}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TabButton.displayName = 'TabButton';
|
@ -0,0 +1,22 @@
|
|||||||
|
import { createSelector } from '@reduxjs/toolkit';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||||
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { memo, useMemo } from 'react';
|
||||||
|
|
||||||
|
export const TabMountGate = memo(({ tab, children }: PropsWithChildren<{ tab: InvokeTabName }>) => {
|
||||||
|
const selectIsTabEnabled = useMemo(
|
||||||
|
() => createSelector(selectConfigSlice, (config) => !config.disabledTabs.includes(tab)),
|
||||||
|
[tab]
|
||||||
|
);
|
||||||
|
const isEnabled = useAppSelector(selectIsTabEnabled);
|
||||||
|
|
||||||
|
if (!isEnabled) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return children;
|
||||||
|
});
|
||||||
|
|
||||||
|
TabMountGate.displayName = 'TabMountGate';
|
@ -0,0 +1,29 @@
|
|||||||
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import type { InvokeTabName } from 'features/ui/store/tabMap';
|
||||||
|
import type { PropsWithChildren } from 'react';
|
||||||
|
import { memo } from 'react';
|
||||||
|
|
||||||
|
export const TabVisibilityGate = memo(({ tab, children }: PropsWithChildren<{ tab: InvokeTabName }>) => {
|
||||||
|
const activeTabName = useAppSelector((s) => s.ui.activeTab);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
display={activeTabName === tab ? undefined : 'none'}
|
||||||
|
pointerEvents={activeTabName === tab ? undefined : 'none'}
|
||||||
|
userSelect={activeTabName === tab ? undefined : 'none'}
|
||||||
|
hidden={activeTabName !== tab}
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
position="absolute"
|
||||||
|
top={0}
|
||||||
|
right={0}
|
||||||
|
bottom={0}
|
||||||
|
left={0}
|
||||||
|
>
|
||||||
|
{children}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
TabVisibilityGate.displayName = 'TabVisibilityGate';
|
@ -0,0 +1,47 @@
|
|||||||
|
import { Flex, Spacer } from '@invoke-ai/ui-library';
|
||||||
|
import { useStore } from '@nanostores/react';
|
||||||
|
import { $customNavComponent } from 'app/store/nanostores/customNavComponent';
|
||||||
|
import InvokeAILogoComponent from 'features/system/components/InvokeAILogoComponent';
|
||||||
|
import SettingsMenu from 'features/system/components/SettingsModal/SettingsMenu';
|
||||||
|
import StatusIndicator from 'features/system/components/StatusIndicator';
|
||||||
|
import { TabMountGate } from 'features/ui/components/TabMountGate';
|
||||||
|
import { memo } from 'react';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { MdZoomOutMap } from 'react-icons/md';
|
||||||
|
import { PiFlowArrowBold } from 'react-icons/pi';
|
||||||
|
import { RiBox2Line, RiInputMethodLine, RiPlayList2Fill } from 'react-icons/ri';
|
||||||
|
|
||||||
|
import { TabButton } from './TabButton';
|
||||||
|
|
||||||
|
export const VerticalNavBar = memo(() => {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const customNavComponent = useStore($customNavComponent);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex flexDir="column" alignItems="center" pt={4} pb={2} gap={4}>
|
||||||
|
<InvokeAILogoComponent />
|
||||||
|
<Flex gap={4} pt={6} h="full" flexDir="column">
|
||||||
|
<TabMountGate tab="generation">
|
||||||
|
<TabButton tab="generation" icon={<RiInputMethodLine />} label={t('ui.tabs.generation')} />
|
||||||
|
</TabMountGate>
|
||||||
|
<TabMountGate tab="upscaling">
|
||||||
|
<TabButton tab="upscaling" icon={<MdZoomOutMap />} label={t('ui.tabs.upscaling')} />
|
||||||
|
</TabMountGate>
|
||||||
|
<TabMountGate tab="workflows">
|
||||||
|
<TabButton tab="workflows" icon={<PiFlowArrowBold />} label={t('ui.tabs.workflows')} />
|
||||||
|
</TabMountGate>
|
||||||
|
<TabMountGate tab="models">
|
||||||
|
<TabButton tab="models" icon={<RiBox2Line />} label={t('ui.tabs.models')} />
|
||||||
|
</TabMountGate>
|
||||||
|
<TabMountGate tab="queue">
|
||||||
|
<TabButton tab="queue" icon={<RiPlayList2Fill />} label={t('ui.tabs.queue')} />
|
||||||
|
</TabMountGate>
|
||||||
|
</Flex>
|
||||||
|
<Spacer />
|
||||||
|
<StatusIndicator />
|
||||||
|
{customNavComponent ? customNavComponent : <SettingsMenu />}
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
VerticalNavBar.displayName = 'VerticalNavBar';
|
@ -1,27 +1,34 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { useAppSelector } from 'app/store/storeHooks';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
|
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
|
||||||
import NodeEditor from 'features/nodes/components/NodeEditor';
|
import NodeEditor from 'features/nodes/components/NodeEditor';
|
||||||
import { memo } from 'react';
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
|
import { memo, useRef } from 'react';
|
||||||
import { ReactFlowProvider } from 'reactflow';
|
import { ReactFlowProvider } from 'reactflow';
|
||||||
|
|
||||||
const NodesTab = () => {
|
const NodesTab = () => {
|
||||||
const mode = useAppSelector((s) => s.workflow.mode);
|
const mode = useAppSelector((s) => s.workflow.mode);
|
||||||
if (mode === 'view') {
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
return (
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
useScopeOnFocus('workflows', ref);
|
||||||
<ImageViewer />
|
|
||||||
<ImageComparisonDroppable />
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box
|
||||||
|
display={activeTabName === 'workflows' ? undefined : 'none'}
|
||||||
|
hidden={activeTabName !== 'workflows'}
|
||||||
|
ref={ref}
|
||||||
|
layerStyle="first"
|
||||||
|
position="relative"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
p={2}
|
||||||
|
borderRadius="base"
|
||||||
|
>
|
||||||
|
{mode === 'edit' && (
|
||||||
<ReactFlowProvider>
|
<ReactFlowProvider>
|
||||||
<NodeEditor />
|
<NodeEditor />
|
||||||
</ReactFlowProvider>
|
</ReactFlowProvider>
|
||||||
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,8 +1,13 @@
|
|||||||
|
import { Flex } from '@invoke-ai/ui-library';
|
||||||
import QueueTabContent from 'features/queue/components/QueueTabContent';
|
import QueueTabContent from 'features/queue/components/QueueTabContent';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const QueueTab = () => {
|
const QueueTab = () => {
|
||||||
return <QueueTabContent />;
|
return (
|
||||||
|
<Flex w="full" h="full">
|
||||||
|
<QueueTabContent />
|
||||||
|
</Flex>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(QueueTab);
|
export default memo(QueueTab);
|
||||||
|
@ -17,7 +17,6 @@ const ResizeHandle = (props: ResizeHandleProps) => {
|
|||||||
<ChakraPanelResizeHandle {...rest}>
|
<ChakraPanelResizeHandle {...rest}>
|
||||||
<Flex sx={sx} data-orientation={orientation}>
|
<Flex sx={sx} data-orientation={orientation}>
|
||||||
<Box className="resize-handle-inner" data-orientation={orientation} />
|
<Box className="resize-handle-inner" data-orientation={orientation} />
|
||||||
<Box className="resize-handle-drag-handle" data-orientation={orientation} />
|
|
||||||
</Flex>
|
</Flex>
|
||||||
</ChakraPanelResizeHandle>
|
</ChakraPanelResizeHandle>
|
||||||
);
|
);
|
||||||
@ -59,22 +58,4 @@ const sx: SystemStyleObject = {
|
|||||||
transitionProperty: 'inherit',
|
transitionProperty: 'inherit',
|
||||||
transitionDuration: 'inherit',
|
transitionDuration: 'inherit',
|
||||||
},
|
},
|
||||||
'.resize-handle-drag-handle': {
|
|
||||||
pos: 'absolute',
|
|
||||||
borderRadius: '1px',
|
|
||||||
transitionProperty: 'inherit',
|
|
||||||
transitionDuration: 'inherit',
|
|
||||||
'&[data-orientation="horizontal"]': {
|
|
||||||
w: '30px',
|
|
||||||
h: '6px',
|
|
||||||
insetInlineStart: '50%',
|
|
||||||
transform: 'translate(-50%, 0)',
|
|
||||||
},
|
|
||||||
'&[data-orientation="vertical"]': {
|
|
||||||
w: '6px',
|
|
||||||
h: '30px',
|
|
||||||
insetBlockStart: '50%',
|
|
||||||
transform: 'translate(0, -50%)',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
};
|
||||||
|
@ -1,21 +1,11 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { ControlLayersEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||||
import { ImageComparisonDroppable } from 'features/gallery/components/ImageViewer/ImageComparisonDroppable';
|
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
|
||||||
import { useImageViewer } from 'features/gallery/components/ImageViewer/useImageViewer';
|
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const TextToImageTab = () => {
|
const TextToImageTab = () => {
|
||||||
const imageViewer = useImageViewer();
|
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
||||||
<ControlLayersEditor />
|
<CanvasEditor />
|
||||||
{imageViewer.isOpen && (
|
|
||||||
<>
|
|
||||||
<ImageViewer />
|
|
||||||
<ImageComparisonDroppable />
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,11 +1,23 @@
|
|||||||
import { Box } from '@invoke-ai/ui-library';
|
import { Box } from '@invoke-ai/ui-library';
|
||||||
import { ImageViewer } from 'features/gallery/components/ImageViewer/ImageViewer';
|
import { useAppSelector } from 'app/store/storeHooks';
|
||||||
|
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
|
||||||
import { memo } from 'react';
|
import { memo } from 'react';
|
||||||
|
|
||||||
const UpscalingTab = () => {
|
const UpscalingTab = () => {
|
||||||
|
const activeTabName = useAppSelector(activeTabNameSelector);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box layerStyle="first" position="relative" w="full" h="full" p={2} borderRadius="base">
|
<Box
|
||||||
<ImageViewer />
|
display={activeTabName === 'upscaling' ? undefined : 'none'}
|
||||||
|
hidden={activeTabName !== 'upscaling'}
|
||||||
|
layerStyle="first"
|
||||||
|
position="relative"
|
||||||
|
w="full"
|
||||||
|
h="full"
|
||||||
|
p={2}
|
||||||
|
borderRadius="base"
|
||||||
|
>
|
||||||
|
{/* <ImageViewer /> */}
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -5,6 +5,8 @@ import type {
|
|||||||
ImperativePanelHandle,
|
ImperativePanelHandle,
|
||||||
PanelOnCollapse,
|
PanelOnCollapse,
|
||||||
PanelOnExpand,
|
PanelOnExpand,
|
||||||
|
PanelProps,
|
||||||
|
PanelResizeHandleProps,
|
||||||
} from 'react-resizable-panels';
|
} from 'react-resizable-panels';
|
||||||
import { getPanelGroupElement, getResizeHandleElementsForGroup } from 'react-resizable-panels';
|
import { getPanelGroupElement, getResizeHandleElementsForGroup } from 'react-resizable-panels';
|
||||||
|
|
||||||
@ -12,6 +14,7 @@ type Direction = 'horizontal' | 'vertical';
|
|||||||
|
|
||||||
export type UsePanelOptions =
|
export type UsePanelOptions =
|
||||||
| {
|
| {
|
||||||
|
id: string;
|
||||||
/**
|
/**
|
||||||
* The minimum size of the panel as a percentage.
|
* The minimum size of the panel as a percentage.
|
||||||
*/
|
*/
|
||||||
@ -24,8 +27,10 @@ export type UsePanelOptions =
|
|||||||
* The unit of the minSize
|
* The unit of the minSize
|
||||||
*/
|
*/
|
||||||
unit: 'percentages';
|
unit: 'percentages';
|
||||||
|
onCollapse?: (isCollapsed: boolean) => void;
|
||||||
}
|
}
|
||||||
| {
|
| {
|
||||||
|
id: string;
|
||||||
/**
|
/**
|
||||||
* The minimum size of the panel in pixels.
|
* The minimum size of the panel in pixels.
|
||||||
*/
|
*/
|
||||||
@ -47,44 +52,18 @@ export type UsePanelOptions =
|
|||||||
* A ref to the panel group.
|
* A ref to the panel group.
|
||||||
*/
|
*/
|
||||||
panelGroupRef: RefObject<ImperativePanelGroupHandle>;
|
panelGroupRef: RefObject<ImperativePanelGroupHandle>;
|
||||||
|
onCollapse?: (isCollapsed: boolean) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type UsePanelReturn = {
|
export type UsePanelReturn = {
|
||||||
/**
|
|
||||||
* The ref to the panel handle.
|
|
||||||
*/
|
|
||||||
ref: RefObject<ImperativePanelHandle>;
|
|
||||||
/**
|
|
||||||
* The dynamically calculated minimum size of the panel.
|
|
||||||
*/
|
|
||||||
minSize: number;
|
|
||||||
/**
|
|
||||||
* The dynamically calculated default size of the panel.
|
|
||||||
*/
|
|
||||||
defaultSize: number;
|
|
||||||
/**
|
/**
|
||||||
* Whether the panel is collapsed.
|
* Whether the panel is collapsed.
|
||||||
*/
|
*/
|
||||||
isCollapsed: boolean;
|
isCollapsed: boolean;
|
||||||
/**
|
|
||||||
* The onCollapse callback. This is required to update the isCollapsed state.
|
|
||||||
* This should be passed to the panel as the onCollapse prop. Wrap it if additional logic is required.
|
|
||||||
*/
|
|
||||||
onCollapse: PanelOnCollapse;
|
|
||||||
/**
|
|
||||||
* The onExpand callback. This is required to update the isCollapsed state.
|
|
||||||
* This should be passed to the panel as the onExpand prop. Wrap it if additional logic is required.
|
|
||||||
*/
|
|
||||||
onExpand: PanelOnExpand;
|
|
||||||
/**
|
/**
|
||||||
* Reset the panel to the minSize.
|
* Reset the panel to the minSize.
|
||||||
*/
|
*/
|
||||||
reset: () => void;
|
reset: () => void;
|
||||||
/**
|
|
||||||
* Reset the panel to the minSize. If the panel is already at the minSize, collapse it.
|
|
||||||
* This should be passed to the `onDoubleClick` prop of the panel's nearest resize handle.
|
|
||||||
*/
|
|
||||||
onDoubleClickHandle: () => void;
|
|
||||||
/**
|
/**
|
||||||
* Toggle the panel between collapsed and expanded.
|
* Toggle the panel between collapsed and expanded.
|
||||||
*/
|
*/
|
||||||
@ -101,6 +80,8 @@ export type UsePanelReturn = {
|
|||||||
* Resize the panel to the given size in the same units as the minSize.
|
* Resize the panel to the given size in the same units as the minSize.
|
||||||
*/
|
*/
|
||||||
resize: (size: number) => void;
|
resize: (size: number) => void;
|
||||||
|
panelProps: Partial<PanelProps & { ref: RefObject<ImperativePanelHandle> }>;
|
||||||
|
resizeHandleProps: Partial<PanelResizeHandleProps>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
||||||
@ -128,12 +109,11 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
|||||||
const minSizePct = getSizeAsPercentage(arg.minSize, arg.panelGroupRef, arg.panelGroupDirection);
|
const minSizePct = getSizeAsPercentage(arg.minSize, arg.panelGroupRef, arg.panelGroupDirection);
|
||||||
_setMinSize(minSizePct);
|
_setMinSize(minSizePct);
|
||||||
|
|
||||||
const defaultSizePct = getSizeAsPercentage(
|
if (arg.defaultSize && arg.defaultSize > minSizePct) {
|
||||||
arg.defaultSize ?? arg.minSize,
|
|
||||||
arg.panelGroupRef,
|
|
||||||
arg.panelGroupDirection
|
|
||||||
);
|
|
||||||
_setDefaultSize(defaultSizePct);
|
_setDefaultSize(defaultSizePct);
|
||||||
|
} else {
|
||||||
|
_setDefaultSize(minSizePct);
|
||||||
|
}
|
||||||
|
|
||||||
if (!panelHandleRef.current.isCollapsed() && panelHandleRef.current.getSize() < minSizePct && minSizePct > 0) {
|
if (!panelHandleRef.current.isCollapsed() && panelHandleRef.current.getSize() < minSizePct && minSizePct > 0) {
|
||||||
panelHandleRef.current.resize(minSizePct);
|
panelHandleRef.current.resize(minSizePct);
|
||||||
@ -144,11 +124,8 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
|||||||
panelGroupHandleElements.forEach((el) => resizeObserver.observe(el));
|
panelGroupHandleElements.forEach((el) => resizeObserver.observe(el));
|
||||||
|
|
||||||
// Resize the panel to the min size once on startup
|
// Resize the panel to the min size once on startup
|
||||||
const defaultSizePct = getSizeAsPercentage(
|
const defaultSizePct =
|
||||||
arg.defaultSize ?? arg.minSize,
|
arg.defaultSize ?? getSizeAsPercentage(arg.minSize, arg.panelGroupRef, arg.panelGroupDirection);
|
||||||
arg.panelGroupRef,
|
|
||||||
arg.panelGroupDirection
|
|
||||||
);
|
|
||||||
panelHandleRef.current?.resize(defaultSizePct);
|
panelHandleRef.current?.resize(defaultSizePct);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
@ -160,11 +137,13 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
|||||||
|
|
||||||
const onCollapse = useCallback<PanelOnCollapse>(() => {
|
const onCollapse = useCallback<PanelOnCollapse>(() => {
|
||||||
setIsCollapsed(true);
|
setIsCollapsed(true);
|
||||||
}, []);
|
arg.onCollapse?.(true);
|
||||||
|
}, [arg]);
|
||||||
|
|
||||||
const onExpand = useCallback<PanelOnExpand>(() => {
|
const onExpand = useCallback<PanelOnExpand>(() => {
|
||||||
setIsCollapsed(false);
|
setIsCollapsed(false);
|
||||||
}, []);
|
arg.onCollapse?.(false);
|
||||||
|
}, [arg]);
|
||||||
|
|
||||||
const toggle = useCallback(() => {
|
const toggle = useCallback(() => {
|
||||||
if (panelHandleRef.current?.isCollapsed()) {
|
if (panelHandleRef.current?.isCollapsed()) {
|
||||||
@ -201,7 +180,7 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
|||||||
panelHandleRef.current?.resize(_minSize);
|
panelHandleRef.current?.resize(_minSize);
|
||||||
}, [_minSize]);
|
}, [_minSize]);
|
||||||
|
|
||||||
const onDoubleClickHandle = useCallback(() => {
|
const cycleState = useCallback(() => {
|
||||||
// If the panel is really super close to the min size, collapse it
|
// If the panel is really super close to the min size, collapse it
|
||||||
if (Math.abs((panelHandleRef.current?.getSize() ?? 0) - _defaultSize) < 0.01) {
|
if (Math.abs((panelHandleRef.current?.getSize() ?? 0) - _defaultSize) < 0.01) {
|
||||||
collapse();
|
collapse();
|
||||||
@ -213,18 +192,23 @@ export const usePanel = (arg: UsePanelOptions): UsePanelReturn => {
|
|||||||
}, [_defaultSize, collapse]);
|
}, [_defaultSize, collapse]);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
ref: panelHandleRef,
|
|
||||||
minSize: _minSize,
|
|
||||||
isCollapsed,
|
isCollapsed,
|
||||||
onCollapse,
|
|
||||||
onExpand,
|
|
||||||
reset,
|
reset,
|
||||||
toggle,
|
toggle,
|
||||||
expand,
|
expand,
|
||||||
collapse,
|
collapse,
|
||||||
resize,
|
resize,
|
||||||
onDoubleClickHandle,
|
panelProps: {
|
||||||
|
id: arg.id,
|
||||||
defaultSize: _defaultSize,
|
defaultSize: _defaultSize,
|
||||||
|
onCollapse,
|
||||||
|
onExpand,
|
||||||
|
ref: panelHandleRef,
|
||||||
|
minSize: _minSize,
|
||||||
|
},
|
||||||
|
resizeHandleProps: {
|
||||||
|
onDoubleClick: cycleState,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -2,6 +2,7 @@ import type { PayloadAction } from '@reduxjs/toolkit';
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PersistConfig, RootState } from 'app/store/store';
|
import type { PersistConfig, RootState } from 'app/store/store';
|
||||||
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
import { workflowLoadRequested } from 'features/nodes/store/actions';
|
||||||
|
import { atom } from 'nanostores';
|
||||||
|
|
||||||
import type { InvokeTabName } from './tabMap';
|
import type { InvokeTabName } from './tabMap';
|
||||||
import type { UIState } from './uiTypes';
|
import type { UIState } from './uiTypes';
|
||||||
@ -77,3 +78,6 @@ export const uiPersistConfig: PersistConfig<UIState> = {
|
|||||||
migrate: migrateUIState,
|
migrate: migrateUIState,
|
||||||
persistDenylist: ['shouldShowImageDetails'],
|
persistDenylist: ['shouldShowImageDetails'],
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const $isGalleryPanelOpen = atom(true);
|
||||||
|
export const $isParametersPanelOpen = atom(true);
|
||||||
|
Loading…
Reference in New Issue
Block a user