feat(ui): clearer viewer/editor context switching

This commit is contained in:
psychedelicious 2024-05-03 12:13:35 +10:00 committed by Kent Keirsey
parent fdfc379a84
commit d9b92d19f9
9 changed files with 174 additions and 127 deletions

View File

@ -88,6 +88,7 @@
"negativePrompt": "Negative Prompt", "negativePrompt": "Negative Prompt",
"discordLabel": "Discord", "discordLabel": "Discord",
"dontAskMeAgain": "Don't ask me again", "dontAskMeAgain": "Don't ask me again",
"editor": "Editor",
"error": "Error", "error": "Error",
"file": "File", "file": "File",
"folder": "Folder", "folder": "Folder",
@ -364,7 +365,7 @@
"bulkDownloadFailed": "Download Failed", "bulkDownloadFailed": "Download Failed",
"problemDeletingImages": "Problem Deleting Images", "problemDeletingImages": "Problem Deleting Images",
"problemDeletingImagesDesc": "One or more images could not be deleted", "problemDeletingImagesDesc": "One or more images could not be deleted",
"backToEditor": "Back to {{tab}} (Esc)" "switchTo": "Switch to {{ tab }} (Z)"
}, },
"hotkeys": { "hotkeys": {
"searchHotkeys": "Search Hotkeys", "searchHotkeys": "Search Hotkeys",

View File

@ -22,6 +22,7 @@ import {
} from 'features/canvas/store/canvasSlice'; } from 'features/canvas/store/canvasSlice';
import type { CanvasLayer } from 'features/canvas/store/canvasTypes'; import type { CanvasLayer } from 'features/canvas/store/canvasTypes';
import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes'; import { LAYER_NAMES_DICT } from 'features/canvas/store/canvasTypes';
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
import { memo, useCallback, useMemo } from 'react'; import { memo, useCallback, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
@ -219,7 +220,11 @@ const IAICanvasToolbar = () => {
const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]); const value = useMemo(() => LAYER_NAMES_DICT.filter((o) => o.value === layer)[0], [layer]);
return ( return (
<Flex alignItems="center" gap={2} flexWrap="wrap"> <Flex w="full" gap={2} alignItems="center">
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto" />
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}> <Tooltip label={`${t('unifiedCanvas.layer')} (Q)`}>
<FormControl isDisabled={isStaging} w="5rem"> <FormControl isDisabled={isStaging} w="5rem">
<Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} /> <Combobox value={value} options={LAYER_NAMES_DICT} onChange={handleChangeLayer} />
@ -311,6 +316,12 @@ const IAICanvasToolbar = () => {
<IAICanvasSettingsButtonPopover /> <IAICanvasSettingsButtonPopover />
</ButtonGroup> </ButtonGroup>
</Flex> </Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<ViewerButton />
</Flex>
</Flex>
</Flex>
); );
}; };

View File

@ -4,16 +4,27 @@ import { BrushSize } from 'features/controlLayers/components/BrushSize';
import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover'; import ControlLayersSettingsPopover from 'features/controlLayers/components/ControlLayersSettingsPopover';
import { ToolChooser } from 'features/controlLayers/components/ToolChooser'; import { ToolChooser } from 'features/controlLayers/components/ToolChooser';
import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup'; import { UndoRedoButtonGroup } from 'features/controlLayers/components/UndoRedoButtonGroup';
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
import { memo } from 'react'; import { memo } from 'react';
export const ControlLayersToolbar = memo(() => { export const ControlLayersToolbar = memo(() => {
return ( return (
<Flex gap={4}> <Flex w="full" gap={2}>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineEnd="auto" />
</Flex>
<Flex flex={1} gap={2} justifyContent="center">
<BrushSize /> <BrushSize />
<ToolChooser /> <ToolChooser />
<UndoRedoButtonGroup /> <UndoRedoButtonGroup />
<ControlLayersSettingsPopover /> <ControlLayersSettingsPopover />
</Flex> </Flex>
<Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto">
<ViewerButton />
</Flex>
</Flex>
</Flex>
); );
}); });

View File

@ -1,24 +0,0 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { PiArrowLeftBold } from 'react-icons/pi';
import { TAB_NAME_TO_TKEY, useImageViewer } from './useImageViewer';
export const BackToEditorButton = () => {
const { t } = useTranslation();
const { onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector);
const tooltip = useMemo(
() => t('gallery.backToEditor', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }),
[t, activeTabName]
);
return (
<Button aria-label={tooltip} tooltip={tooltip} onClick={onClose} leftIcon={<PiArrowLeftBold />} variant="ghost">
{t('common.back')}
</Button>
);
};

View File

@ -0,0 +1,37 @@
import { Button } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { activeTabNameSelector } from 'features/ui/store/uiSelectors';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useImageViewer } from './useImageViewer';
export const TAB_NAME_TO_TKEY: Record<InvokeTabName, string> = {
generation: 'ui.tabs.generationTab',
canvas: 'ui.tabs.canvasTab',
workflows: 'ui.tabs.workflowsTab',
models: 'ui.tabs.modelsTab',
queue: 'ui.tabs.queueTab',
};
export const TAB_NAME_TO_TKEY_SHORT: Record<InvokeTabName, string> = {
generation: 'ui.tabs.generation',
canvas: 'ui.tabs.canvas',
workflows: 'ui.tabs.workflows',
models: 'ui.tabs.models',
queue: 'ui.tabs.queue',
};
export const EditorButton = () => {
const { t } = useTranslation();
const { onClose } = useImageViewer();
const activeTabName = useAppSelector(activeTabNameSelector);
const tooltip = useMemo(() => t('gallery.switchTo', { tab: t(TAB_NAME_TO_TKEY[activeTabName]) }), [t, activeTabName]);
return (
<Button aria-label={tooltip} tooltip={tooltip} onClick={onClose} variant="ghost">
{t(TAB_NAME_TO_TKEY_SHORT[activeTabName])}
</Button>
);
};

View File

@ -10,9 +10,9 @@ import { AnimatePresence, motion } from 'framer-motion';
import { memo, useMemo } from 'react'; import { memo, useMemo } from 'react';
import { useHotkeys } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook';
import { BackToEditorButton } from './BackToEditorButton';
import CurrentImageButtons from './CurrentImageButtons'; import CurrentImageButtons from './CurrentImageButtons';
import CurrentImagePreview from './CurrentImagePreview'; import CurrentImagePreview from './CurrentImagePreview';
import { EditorButton } from './EditorButton';
const initial: AnimationProps['initial'] = { const initial: AnimationProps['initial'] = {
opacity: 0, opacity: 0,
@ -39,13 +39,14 @@ export const ImageViewer = memo(() => {
return isOpen; return isOpen;
}, [isOpen, isViewerEnabled]); }, [isOpen, isViewerEnabled]);
useHotkeys('shift+s', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]); useHotkeys('z', onToggle, { enabled: isViewerEnabled }, [isViewerEnabled, onToggle]);
useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]); useHotkeys('esc', onClose, { enabled: isViewerEnabled }, [isViewerEnabled, onClose]);
return ( return (
<AnimatePresence> <AnimatePresence>
{shouldShowViewer && ( {shouldShowViewer && (
<Flex <Flex
key="imageViewer"
as={motion.div} as={motion.div}
initial={initial} initial={initial}
animate={animate} animate={animate}
@ -76,7 +77,7 @@ export const ImageViewer = memo(() => {
</Flex> </Flex>
<Flex flex={1} justifyContent="center"> <Flex flex={1} justifyContent="center">
<Flex gap={2} marginInlineStart="auto"> <Flex gap={2} marginInlineStart="auto">
<BackToEditorButton /> <EditorButton />
</Flex> </Flex>
</Flex> </Flex>
</Flex> </Flex>

View File

@ -0,0 +1,17 @@
import { Button } from '@invoke-ai/ui-library';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { useImageViewer } from './useImageViewer';
export const ViewerButton = () => {
const { t } = useTranslation();
const { onOpen } = useImageViewer();
const tooltip = useMemo(() => t('gallery.switchTo', { tab: t('common.viewer') }), [t]);
return (
<Button aria-label={tooltip} tooltip={tooltip} onClick={onOpen} variant="ghost" pointerEvents="auto">
{t('common.viewer')}
</Button>
);
};

View File

@ -1,16 +1,7 @@
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
import type { InvokeTabName } from 'features/ui/store/tabMap';
import { useCallback } from 'react'; import { useCallback } from 'react';
export const TAB_NAME_TO_TKEY: Record<InvokeTabName, string> = {
generation: 'ui.tabs.generation',
canvas: 'ui.tabs.canvas',
workflows: 'ui.tabs.workflows',
models: 'ui.tabs.models',
queue: 'ui.tabs.queue',
};
export const useImageViewer = () => { export const useImageViewer = () => {
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen); const isOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);

View File

@ -1,5 +1,6 @@
import { Flex, Spacer } from '@invoke-ai/ui-library'; import { Flex, Spacer } from '@invoke-ai/ui-library';
import { useAppSelector } from 'app/store/storeHooks'; import { useAppSelector } from 'app/store/storeHooks';
import { ViewerButton } from 'features/gallery/components/ImageViewer/ViewerButton';
import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton'; import AddNodeButton from 'features/nodes/components/flow/panels/TopPanel/AddNodeButton';
import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton'; import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/ClearFlowButton';
import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton';
@ -22,6 +23,7 @@ const TopCenterPanel = () => {
<ClearFlowButton /> <ClearFlowButton />
<SaveWorkflowButton /> <SaveWorkflowButton />
<WorkflowLibraryMenu /> <WorkflowLibraryMenu />
<ViewerButton />
</Flex> </Flex>
); );
}; };