mirror of
https://github.com/invoke-ai/InvokeAI
synced 2024-08-30 20:32:17 +00:00
perf(ui): optimize all selectors 1
I learned that the inline selector syntax recreates the selector function on every render: ```ts const val = useAppSelector((s) => s.slice.val) ``` Not good! Better is to create a selector outside the function and use it. Doing that for all selectors now, most of the way through now. Feels snappier.
This commit is contained in:
parent
f126a61f66
commit
a41406ca9a
@ -1,5 +1,7 @@
|
||||
import { Button, Flex, Heading, Image, Link, Text } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import newGithubIssueUrl from 'new-github-issue-url';
|
||||
import InvokeLogoYellow from 'public/assets/images/invoke-symbol-ylw-lrg.svg';
|
||||
@ -13,9 +15,11 @@ type Props = {
|
||||
resetErrorBoundary: () => void;
|
||||
};
|
||||
|
||||
const selectIsLocal = createSelector(selectConfigSlice, (config) => config.isLocal);
|
||||
|
||||
const AppErrorBoundaryFallback = ({ error, resetErrorBoundary }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const isLocal = useAppSelector((s) => s.config.isLocal);
|
||||
const isLocal = useAppSelector(selectIsLocal);
|
||||
|
||||
const handleCopy = useCallback(() => {
|
||||
const text = JSON.stringify(serializeError(error), null, 2);
|
||||
|
@ -1,15 +1,21 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createLogWriter } from '@roarr/browser-log-writer';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { useEffect, useMemo } from 'react';
|
||||
import { ROARR, Roarr } from 'roarr';
|
||||
|
||||
import type { LogNamespace } from './logger';
|
||||
import { $logger, BASE_CONTEXT, LOG_LEVEL_MAP, logger } from './logger';
|
||||
|
||||
const selectLogLevel = createSelector(selectSystemSlice, (system) => system.logLevel);
|
||||
const selectLogNamespaces = createSelector(selectSystemSlice, (system) => system.logNamespaces);
|
||||
const selectLogIsEnabled = createSelector(selectSystemSlice, (system) => system.logIsEnabled);
|
||||
|
||||
export const useLogger = (namespace: LogNamespace) => {
|
||||
const logLevel = useAppSelector((s) => s.system.logLevel);
|
||||
const logNamespaces = useAppSelector((s) => s.system.logNamespaces);
|
||||
const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled);
|
||||
const logLevel = useAppSelector(selectLogLevel);
|
||||
const logNamespaces = useAppSelector(selectLogNamespaces);
|
||||
const logIsEnabled = useAppSelector(selectLogIsEnabled);
|
||||
|
||||
// The provided Roarr browser log writer uses localStorage to config logging to console
|
||||
useEffect(() => {
|
||||
|
@ -13,8 +13,9 @@ import {
|
||||
Spacer,
|
||||
Text,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice';
|
||||
import { selectSystemSlice, setShouldEnableInformationalPopovers } from 'features/system/store/systemSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { merge, omit } from 'lodash-es';
|
||||
import type { ReactElement } from 'react';
|
||||
@ -31,8 +32,10 @@ type Props = {
|
||||
children: ReactElement;
|
||||
};
|
||||
|
||||
const selectShouldEnableInformationalPopovers = createSelector(selectSystemSlice, system => system.shouldEnableInformationalPopovers);
|
||||
|
||||
export const InformationalPopover = memo(({ feature, children, inPortal = true, ...rest }: Props) => {
|
||||
const shouldEnableInformationalPopovers = useAppSelector((s) => s.system.shouldEnableInformationalPopovers);
|
||||
const shouldEnableInformationalPopovers = useAppSelector(selectShouldEnableInformationalPopovers);
|
||||
|
||||
const data = useMemo(() => POPOVER_DATA[feature], [feature]);
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@ -26,7 +27,7 @@ const selectPostUploadAction = createMemoizedSelector(selectActiveTab, (activeTa
|
||||
|
||||
export const useFullscreenDropzone = () => {
|
||||
const { t } = useTranslation();
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const [isHandlingUpload, setIsHandlingUpload] = useState<boolean>(false);
|
||||
const postUploadAction = useAppSelector(selectPostUploadAction);
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { GroupBase } from 'chakra-react-select';
|
||||
import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { ModelIdentifierField } from 'features/nodes/types/common';
|
||||
import { groupBy, reduce } from 'lodash-es';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
@ -28,11 +30,13 @@ const groupByBaseFunc = <T extends AnyModelConfig>(model: T) => model.base.toUpp
|
||||
const groupByBaseAndTypeFunc = <T extends AnyModelConfig>(model: T) =>
|
||||
`${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`;
|
||||
|
||||
const selectBaseWithSDXLFallback = createSelector(selectParamsSlice, (params) => params.model?.base ?? 'sdxl');
|
||||
|
||||
export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
||||
arg: UseGroupedModelComboboxArg<T>
|
||||
): UseGroupedModelComboboxReturn => {
|
||||
const { t } = useTranslation();
|
||||
const base_model = useAppSelector((s) => s.params.model?.base ?? 'sdxl');
|
||||
const base = useAppSelector(selectBaseWithSDXLFallback);
|
||||
const { modelConfigs, selectedModel, getIsDisabled, onChange, isLoading, groupByType = false } = arg;
|
||||
const options = useMemo<GroupBase<ComboboxOption>[]>(() => {
|
||||
if (!modelConfigs) {
|
||||
@ -54,9 +58,9 @@ export const useGroupedModelCombobox = <T extends AnyModelConfig>(
|
||||
},
|
||||
[] as GroupBase<ComboboxOption>[]
|
||||
);
|
||||
_options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base_model) ? -1 : 1));
|
||||
_options.sort((a) => (a.label?.split('/')[0]?.toLowerCase().includes(base) ? -1 : 1));
|
||||
return _options;
|
||||
}, [modelConfigs, groupByType, getIsDisabled, base_model]);
|
||||
}, [modelConfigs, groupByType, getIsDisabled, base]);
|
||||
|
||||
const value = useMemo(
|
||||
() =>
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { useCallback } from 'react';
|
||||
import { useDropzone } from 'react-dropzone';
|
||||
import { useUploadImageMutation } from 'services/api/endpoints/images';
|
||||
@ -29,7 +30,7 @@ type UseImageUploadButtonArgs = {
|
||||
* <input {...getUploadInputProps()} /> // hidden, handles native upload functionality
|
||||
*/
|
||||
export const useImageUploadButton = ({ postUploadAction, isDisabled }: UseImageUploadButtonArgs) => {
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const [uploadImage] = useUploadImageMutation();
|
||||
const onDropAccepted = useCallback(
|
||||
(files: File[]) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, ConfirmationAlertDialog, Flex, FormControl, Text } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import {
|
||||
@ -18,12 +19,17 @@ const selectImagesToChange = createMemoizedSelector(
|
||||
(changeBoardModal) => changeBoardModal.imagesToChange
|
||||
);
|
||||
|
||||
const selectIsModalOpen = createSelector(
|
||||
selectChangeBoardModalSlice,
|
||||
(changeBoardModal) => changeBoardModal.isModalOpen
|
||||
);
|
||||
|
||||
const ChangeBoardModal = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [selectedBoard, setSelectedBoard] = useState<string | null>();
|
||||
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||
const { data: boards, isFetching } = useListAllBoardsQuery(queryArgs);
|
||||
const isModalOpen = useAppSelector((s) => s.changeBoardModal.isModalOpen);
|
||||
const isModalOpen = useAppSelector(selectIsModalOpen);
|
||||
const imagesToChange = useAppSelector(selectImagesToChange);
|
||||
const [addImagesToBoard] = useAddImagesToBoardMutation();
|
||||
const [removeImagesFromBoard] = useRemoveImagesFromBoardMutation();
|
||||
|
@ -8,7 +8,7 @@ import {
|
||||
rasterLayerAdded,
|
||||
rgAdded,
|
||||
} from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
@ -16,10 +16,7 @@ import { PiPlusBold, PiTrashSimpleBold } from 'react-icons/pi';
|
||||
export const CanvasEntityListMenuItems = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const hasEntities = useAppSelector((s) => {
|
||||
const count = selectEntityCount(s);
|
||||
return count > 0;
|
||||
});
|
||||
const hasEntities = useAppSelector(selectHasEntities);
|
||||
const addInpaintMask = useCallback(() => {
|
||||
dispatch(inpaintMaskAdded({ isSelected: true }));
|
||||
}, [dispatch]);
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { Button, ButtonGroup } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { selectCanvasSessionSlice, sessionModeChanged } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectCanvasMode = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.mode);
|
||||
|
||||
export const CanvasModeSwitcher = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const mode = useAppSelector((s) => s.canvasSession.mode);
|
||||
const mode = useAppSelector(selectCanvasMode);
|
||||
const onClickGenerate = useCallback(() => dispatch(sessionModeChanged({ mode: 'generate' })), [dispatch]);
|
||||
const onClickCompose = useCallback(() => dispatch(sessionModeChanged({ mode: 'compose' })), [dispatch]);
|
||||
|
||||
|
@ -4,11 +4,11 @@ import { CanvasAddEntityButtons } from 'features/controlLayers/components/Canvas
|
||||
import { CanvasEntityList } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityList';
|
||||
import { CanvasEntityListMenuItems } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems';
|
||||
import { CanvasManagerProviderGate } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { selectEntityCount } from 'features/controlLayers/store/selectors';
|
||||
import { selectHasEntities } from 'features/controlLayers/store/selectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
|
||||
export const CanvasPanelContent = memo(() => {
|
||||
const hasEntities = useAppSelector((s) => selectEntityCount(s) > 0);
|
||||
const hasEntities = useAppSelector(selectHasEntities);
|
||||
const renderMenu = useCallback(
|
||||
() => (
|
||||
<MenuList>
|
||||
|
@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import { useEntityIdentifierContext } from 'features/controlLayers/contexts/EntityIdentifierContext';
|
||||
import { selectBase } from 'features/controlLayers/store/paramsSlice';
|
||||
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -18,7 +19,7 @@ export const ControlLayerControlAdapterModel = memo(({ modelKey, onChange: onCha
|
||||
const { t } = useTranslation();
|
||||
const entityIdentifier = useEntityIdentifierContext();
|
||||
const canvasManager = useCanvasManager();
|
||||
const currentBaseModel = useAppSelector((s) => s.params.model?.base);
|
||||
const currentBaseModel = useAppSelector(selectBase);
|
||||
const [modelConfigs, { isLoading }] = useControlNetAndT2IAdapterModels();
|
||||
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);
|
||||
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { CanvasEntityGroupList } from 'features/controlLayers/components/common/CanvasEntityGroupList';
|
||||
@ -10,8 +11,12 @@ const selectEntityIds = createMemoizedSelector(selectCanvasSlice, (canvas) => {
|
||||
return canvas.controlLayers.entities.map(mapId).reverse();
|
||||
});
|
||||
|
||||
const selectIsSelected = createSelector(selectSelectedEntityIdentifier, (selectedEntityIdentifier) => {
|
||||
return selectedEntityIdentifier?.type === 'control_layer';
|
||||
});
|
||||
|
||||
export const ControlLayerEntityList = memo(() => {
|
||||
const isSelected = useAppSelector((s) => selectSelectedEntityIdentifier(s)?.type === 'control_layer');
|
||||
const isSelected = useAppSelector(selectIsSelected);
|
||||
const layerIds = useAppSelector(selectEntityIds);
|
||||
|
||||
if (layerIds.length === 0) {
|
||||
|
@ -1,20 +1,17 @@
|
||||
import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||
import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import type { FilterConfig } from 'features/controlLayers/store/types';
|
||||
import { IMAGE_FILTERS, isFilterType } from 'features/controlLayers/store/types';
|
||||
import { configSelector } from 'features/system/store/configSelectors';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { includes, map } from 'lodash-es';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const selectDisabledProcessors = createMemoizedSelector(
|
||||
configSelector,
|
||||
(config) => config.sd.disabledControlNetProcessors
|
||||
);
|
||||
const selectDisabledProcessors = createSelector(selectConfigSlice, (config) => config.sd.disabledControlNetProcessors);
|
||||
|
||||
type Props = {
|
||||
filterType: FilterConfig['type'];
|
||||
|
@ -2,6 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library';
|
||||
import { Combobox, Flex, FormControl, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
import { selectBase } from 'features/controlLayers/store/paramsSlice';
|
||||
import type { CLIPVisionModelV2 } from 'features/controlLayers/store/types';
|
||||
import { isCLIPVisionModelV2 } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@ -24,7 +25,7 @@ type Props = {
|
||||
|
||||
export const IPAdapterModel = memo(({ modelKey, onChangeModel, clipVisionModel, onChangeCLIPVisionModel }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const currentBaseModel = useAppSelector((s) => s.params.model?.base);
|
||||
const currentBaseModel = useAppSelector(selectBase);
|
||||
const [modelConfigs, { isLoading }] = useIPAdapterModels();
|
||||
const selectedModel = useMemo(() => modelConfigs.find((m) => m.key === modelKey), [modelConfigs, modelKey]);
|
||||
|
||||
|
@ -1,13 +1,16 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { selectCanvasSettingsSlice, settingsAutoSaveToggled } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectAutoSave = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.autoSave);
|
||||
|
||||
export const CanvasSettingsAutoSaveCheckbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const autoSave = useAppSelector((s) => s.canvasSettings.autoSave);
|
||||
const autoSave = useAppSelector(selectAutoSave);
|
||||
const onChange = useCallback(() => dispatch(settingsAutoSaveToggled()), [dispatch]);
|
||||
return (
|
||||
<FormControl w="full">
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { clipToBboxChanged } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { clipToBboxChanged, selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectClipToBbox = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.clipToBbox);
|
||||
|
||||
export const CanvasSettingsClipToBboxCheckbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const clipToBbox = useAppSelector((s) => s.canvasSettings.clipToBbox);
|
||||
const clipToBbox = useAppSelector(selectClipToBbox);
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(clipToBboxChanged(e.target.checked)),
|
||||
[dispatch]
|
||||
|
@ -1,13 +1,19 @@
|
||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { settingsDynamicGridToggled } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import {
|
||||
selectCanvasSettingsSlice,
|
||||
settingsDynamicGridToggled,
|
||||
} from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||
|
||||
export const CanvasSettingsDynamicGridSwitch = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid);
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
const onChange = useCallback(() => {
|
||||
dispatch(settingsDynamicGridToggled());
|
||||
}, [dispatch]);
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { invertScrollChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import { invertScrollChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectInvertScroll = createSelector(selectToolSlice, (tool) => tool.invertScroll);
|
||||
|
||||
export const CanvasSettingsInvertScrollCheckbox = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const invertScroll = useAppSelector((s) => s.tool.invertScroll);
|
||||
const invertScroll = useAppSelector(selectInvertScroll);
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(invertScrollChanged(e.target.checked)),
|
||||
[dispatch]
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { $socket } from 'app/hooks/useSocketIO';
|
||||
import { logger } from 'app/logging/logger';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
@ -7,6 +8,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { HeadsUpDisplay } from 'features/controlLayers/components/HeadsUpDisplay';
|
||||
import { CanvasManager } from 'features/controlLayers/konva/CanvasManager';
|
||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||
import { selectCanvasSettingsSlice } from 'features/controlLayers/store/canvasSettingsSlice';
|
||||
import Konva from 'konva';
|
||||
import { memo, useCallback, useEffect, useLayoutEffect, useState } from 'react';
|
||||
import { useDevicePixelRatio } from 'use-device-pixel-ratio';
|
||||
@ -51,8 +53,10 @@ type Props = {
|
||||
asPreview?: boolean;
|
||||
};
|
||||
|
||||
const selectDynamicGrid = createSelector(selectCanvasSettingsSlice, (canvasSettings) => canvasSettings.dynamicGrid);
|
||||
|
||||
export const StageComponent = memo(({ asPreview = false }: Props) => {
|
||||
const dynamicGrid = useAppSelector((s) => s.canvasSettings.dynamicGrid);
|
||||
const dynamicGrid = useAppSelector(selectDynamicGrid);
|
||||
|
||||
const [stage] = useState(
|
||||
() =>
|
||||
|
@ -1,9 +1,10 @@
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import type { PropsWithChildren } from 'react';
|
||||
import { memo } from 'react';
|
||||
|
||||
export const StagingAreaIsStagingGate = memo((props: PropsWithChildren) => {
|
||||
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
|
||||
if (!isStaging) {
|
||||
return null;
|
||||
|
@ -1,9 +1,11 @@
|
||||
import { Button, ButtonGroup, IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { INTERACTION_SCOPES, useScopeOnMount } from 'common/hooks/interactionScopes';
|
||||
import { useCanvasManager } from 'features/controlLayers/contexts/CanvasManagerProviderGate';
|
||||
import {
|
||||
selectCanvasSessionSlice,
|
||||
sessionNextStagedImageSelected,
|
||||
sessionPrevStagedImageSelected,
|
||||
sessionStagedImageDiscarded,
|
||||
@ -25,15 +27,25 @@ import {
|
||||
} from 'react-icons/pi';
|
||||
import { useChangeImageIsIntermediateMutation } from 'services/api/endpoints/images';
|
||||
|
||||
const selectStagedImageIndex = createSelector(
|
||||
selectCanvasSessionSlice,
|
||||
(canvasSession) => canvasSession.selectedStagedImageIndex
|
||||
);
|
||||
|
||||
const selectSelectedImage = createSelector(
|
||||
[selectCanvasSessionSlice, selectStagedImageIndex],
|
||||
(canvasSession, index) => canvasSession.stagedImages[index] ?? null
|
||||
);
|
||||
|
||||
const selectImageCount = createSelector(selectCanvasSessionSlice, (canvasSession) => canvasSession.stagedImages.length);
|
||||
|
||||
export const StagingAreaToolbar = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const session = useAppSelector((s) => s.canvasSession);
|
||||
const canvasManager = useCanvasManager();
|
||||
const index = useAppSelector(selectStagedImageIndex);
|
||||
const selectedImage = useAppSelector(selectSelectedImage);
|
||||
const imageCount = useAppSelector(selectImageCount);
|
||||
const shouldShowStagedImage = useStore(canvasManager.stateApi.$shouldShowStagedImage);
|
||||
const images = useMemo(() => session.stagedImages, [session]);
|
||||
const selectedImage = useMemo(() => {
|
||||
return images[session.selectedStagedImageIndex] ?? null;
|
||||
}, [images, session.selectedStagedImageIndex]);
|
||||
const isCanvasActive = useStore(INTERACTION_SCOPES.canvas.$isActive);
|
||||
const [changeIsImageIntermediate] = useChangeImageIsIntermediateMutation();
|
||||
useScopeOnMount('stagingArea');
|
||||
@ -52,19 +64,19 @@ export const StagingAreaToolbar = memo(() => {
|
||||
if (!selectedImage) {
|
||||
return;
|
||||
}
|
||||
dispatch(sessionStagingAreaImageAccepted({ index: session.selectedStagedImageIndex }));
|
||||
}, [dispatch, selectedImage, session.selectedStagedImageIndex]);
|
||||
dispatch(sessionStagingAreaImageAccepted({ index }));
|
||||
}, [dispatch, index, selectedImage]);
|
||||
|
||||
const onDiscardOne = useCallback(() => {
|
||||
if (!selectedImage) {
|
||||
return;
|
||||
}
|
||||
if (images.length === 1) {
|
||||
if (imageCount === 1) {
|
||||
dispatch(sessionStagingAreaReset());
|
||||
} else {
|
||||
dispatch(sessionStagedImageDiscarded({ index: session.selectedStagedImageIndex }));
|
||||
dispatch(sessionStagedImageDiscarded({ index }));
|
||||
}
|
||||
}, [selectedImage, images.length, dispatch, session.selectedStagedImageIndex]);
|
||||
}, [selectedImage, imageCount, dispatch, index]);
|
||||
|
||||
const onDiscardAll = useCallback(() => {
|
||||
dispatch(sessionStagingAreaReset());
|
||||
@ -112,12 +124,12 @@ export const StagingAreaToolbar = memo(() => {
|
||||
);
|
||||
|
||||
const counterText = useMemo(() => {
|
||||
if (images.length > 0) {
|
||||
return `${(session.selectedStagedImageIndex ?? 0) + 1} of ${images.length}`;
|
||||
if (imageCount > 0) {
|
||||
return `${(index ?? 0) + 1} of ${imageCount}`;
|
||||
} else {
|
||||
return `0 of 0`;
|
||||
}
|
||||
}, [images.length, session.selectedStagedImageIndex]);
|
||||
}, [imageCount, index]);
|
||||
|
||||
return (
|
||||
<>
|
||||
@ -128,7 +140,7 @@ export const StagingAreaToolbar = memo(() => {
|
||||
icon={<PiArrowLeftBold />}
|
||||
onClick={onPrev}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={images.length <= 1 || !shouldShowStagedImage}
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
<Button colorScheme="base" pointerEvents="none" minW={28}>
|
||||
{counterText}
|
||||
@ -139,7 +151,7 @@ export const StagingAreaToolbar = memo(() => {
|
||||
icon={<PiArrowRightBold />}
|
||||
onClick={onNext}
|
||||
colorScheme="invokeBlue"
|
||||
isDisabled={images.length <= 1 || !shouldShowStagedImage}
|
||||
isDisabled={imageCount <= 1 || !shouldShowStagedImage}
|
||||
/>
|
||||
</ButtonGroup>
|
||||
<ButtonGroup borderRadius="base" shadow="dark-lg">
|
||||
|
@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -14,7 +15,7 @@ export const ToolBboxButton = memo(() => {
|
||||
const isSelected = useToolIsSelected('bbox');
|
||||
const isFiltering = useIsFiltering();
|
||||
const isTransforming = useIsTransforming();
|
||||
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const isDisabled = useMemo(() => {
|
||||
return isTransforming || isFiltering || isStaging;
|
||||
}, [isFiltering, isStaging, isTransforming]);
|
||||
|
@ -9,18 +9,20 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { brushWidthChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import { brushWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const marks = [0, 100, 200, 300];
|
||||
const formatPx = (v: number | string) => `${v} px`;
|
||||
const selectBrushWidth = createSelector(selectToolSlice, (tool) => tool.brush.width);
|
||||
|
||||
export const ToolBrushWidth = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const width = useAppSelector((s) => s.tool.brush.width);
|
||||
const width = useAppSelector(selectBrushWidth);
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(brushWidthChanged(Math.round(v)));
|
||||
|
@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -14,7 +15,7 @@ export const ToolColorPickerButton = memo(() => {
|
||||
const isTransforming = useIsTransforming();
|
||||
const selectColorPicker = useSelectTool('colorPicker');
|
||||
const isSelected = useToolIsSelected('colorPicker');
|
||||
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
|
||||
const isDisabled = useMemo(() => {
|
||||
return isTransforming || isFiltering || isStaging;
|
||||
|
@ -9,18 +9,20 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { eraserWidthChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import { eraserWidthChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const marks = [0, 100, 200, 300];
|
||||
const formatPx = (v: number | string) => `${v} px`;
|
||||
const selectEraserWidth = createSelector(selectToolSlice, (tool) => tool.eraser.width);
|
||||
|
||||
export const ToolEraserWidth = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const width = useAppSelector((s) => s.tool.eraser.width);
|
||||
const width = useAppSelector(selectEraserWidth);
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
dispatch(eraserWidthChanged(Math.round(v)));
|
||||
|
@ -1,15 +1,18 @@
|
||||
import { Flex, Popover, PopoverBody, PopoverContent, PopoverTrigger } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIColorPicker from 'common/components/IAIColorPicker';
|
||||
import { rgbaColorToString } from 'common/util/colorCodeTransformers';
|
||||
import { fillChanged } from 'features/controlLayers/store/toolSlice';
|
||||
import { fillChanged, selectToolSlice } from 'features/controlLayers/store/toolSlice';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectFill = createSelector(selectToolSlice, (tool) => tool.fill);
|
||||
|
||||
export const ToolFillColorPicker = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const fill = useAppSelector((s) => s.tool.fill);
|
||||
const fill = useAppSelector(selectFill);
|
||||
const dispatch = useAppDispatch();
|
||||
const onChange = useCallback(
|
||||
(color: RgbaColor) => {
|
||||
|
@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useSelectTool, useToolIsSelected } from 'features/controlLayers/components/Tool/hooks';
|
||||
import { useIsFiltering } from 'features/controlLayers/hooks/useIsFiltering';
|
||||
import { useIsTransforming } from 'features/controlLayers/hooks/useIsTransforming';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -12,7 +13,7 @@ export const ToolViewButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isTransforming = useIsTransforming();
|
||||
const isFiltering = useIsFiltering();
|
||||
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
const selectView = useSelectTool('view');
|
||||
const isSelected = useToolIsSelected('view');
|
||||
const isDisabled = useMemo(() => {
|
||||
|
@ -13,6 +13,7 @@ import {
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { snapToNearest } from 'features/controlLayers/konva/util';
|
||||
import { entityOpacityChanged } from 'features/controlLayers/store/canvasSlice';
|
||||
@ -60,27 +61,29 @@ const sliderDefaultValue = mapOpacityToSliderValue(100);
|
||||
|
||||
const snapCandidates = marks.slice(1, marks.length - 1);
|
||||
|
||||
const selectOpacity = createSelector(selectCanvasSlice, (canvas) => {
|
||||
const selectedEntityIdentifier = canvas.selectedEntityIdentifier;
|
||||
if (!selectedEntityIdentifier) {
|
||||
return 100; // fallback to 100% opacity
|
||||
}
|
||||
const selectedEntity = selectEntity(canvas, selectedEntityIdentifier);
|
||||
if (!selectedEntity) {
|
||||
return 100; // fallback to 100% opacity
|
||||
}
|
||||
if (!isDrawableEntity(selectedEntity)) {
|
||||
return 100; // fallback to 100% opacity
|
||||
}
|
||||
// Opacity is a float from 0-1, but we want to display it as a percentage
|
||||
return selectedEntity.opacity * 100;
|
||||
});
|
||||
|
||||
export const CanvasEntityOpacity = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const opacity = useAppSelector((s) => {
|
||||
const selectedEntityIdentifier = selectSelectedEntityIdentifier(s);
|
||||
if (!selectedEntityIdentifier) {
|
||||
return null;
|
||||
}
|
||||
const canvas = selectCanvasSlice(s);
|
||||
const selectedEntity = selectEntity(canvas, selectedEntityIdentifier);
|
||||
if (!selectedEntity) {
|
||||
return null;
|
||||
}
|
||||
if (!isDrawableEntity(selectedEntity)) {
|
||||
return null;
|
||||
}
|
||||
return selectedEntity.opacity;
|
||||
});
|
||||
const opacity = useAppSelector(selectOpacity);
|
||||
|
||||
const [localOpacity, setLocalOpacity] = useState((opacity ?? 1) * 100);
|
||||
const [localOpacity, setLocalOpacity] = useState(opacity);
|
||||
|
||||
const onChangeSlider = useCallback(
|
||||
(opacity: number) => {
|
||||
|
@ -1,6 +1,8 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -12,15 +14,11 @@ type Props = {
|
||||
const formatValue = (v: number) => v.toFixed(2);
|
||||
const marks = [0, 1, 2];
|
||||
|
||||
const selectWeightConfig = createSelector(selectConfigSlice, (config) => config.sd.ca.weight);
|
||||
|
||||
export const Weight = memo(({ weight, onChange }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const initial = useAppSelector((s) => s.config.sd.ca.weight.initial);
|
||||
const sliderMin = useAppSelector((s) => s.config.sd.ca.weight.sliderMin);
|
||||
const sliderMax = useAppSelector((s) => s.config.sd.ca.weight.sliderMax);
|
||||
const numberInputMin = useAppSelector((s) => s.config.sd.ca.weight.numberInputMin);
|
||||
const numberInputMax = useAppSelector((s) => s.config.sd.ca.weight.numberInputMax);
|
||||
const coarseStep = useAppSelector((s) => s.config.sd.ca.weight.coarseStep);
|
||||
const fineStep = useAppSelector((s) => s.config.sd.ca.weight.fineStep);
|
||||
const config = useAppSelector(selectWeightConfig);
|
||||
|
||||
return (
|
||||
<FormControl orientation="horizontal">
|
||||
@ -30,23 +28,23 @@ export const Weight = memo(({ weight, onChange }: Props) => {
|
||||
<CompositeSlider
|
||||
value={weight}
|
||||
onChange={onChange}
|
||||
defaultValue={initial}
|
||||
min={sliderMin}
|
||||
max={sliderMax}
|
||||
step={coarseStep}
|
||||
fineStep={fineStep}
|
||||
defaultValue={config.initial}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
marks={marks}
|
||||
formatValue={formatValue}
|
||||
/>
|
||||
<CompositeNumberInput
|
||||
value={weight}
|
||||
onChange={onChange}
|
||||
min={numberInputMin}
|
||||
max={numberInputMax}
|
||||
step={coarseStep}
|
||||
fineStep={fineStep}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
maxW={20}
|
||||
defaultValue={initial}
|
||||
defaultValue={config.initial}
|
||||
/>
|
||||
</FormControl>
|
||||
);
|
||||
|
@ -1,21 +1,16 @@
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectIsStaging } from 'features/controlLayers/store/canvasSessionSlice';
|
||||
import { entityDeleted } from 'features/controlLayers/store/canvasSlice';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
import { selectSelectedEntityIdentifier } from 'features/controlLayers/store/selectors';
|
||||
import { useCallback, useMemo } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
|
||||
const selectSelectedEntityIdentifier = createMemoizedSelector(
|
||||
selectCanvasSlice,
|
||||
(canvasState) => canvasState.selectedEntityIdentifier
|
||||
);
|
||||
|
||||
export function useCanvasDeleteLayerHotkey() {
|
||||
useAssertSingleton(useCanvasDeleteLayerHotkey.name);
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedEntityIdentifier = useAppSelector(selectSelectedEntityIdentifier);
|
||||
const isStaging = useAppSelector((s) => s.canvasSession.isStaging);
|
||||
const isStaging = useAppSelector(selectIsStaging);
|
||||
|
||||
const deleteSelectedLayer = useCallback(() => {
|
||||
if (selectedEntityIdentifier === null) {
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createMemoizedAppSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { deepClone } from 'common/util/deepClone';
|
||||
import { selectBase } from 'features/controlLayers/store/paramsSlice';
|
||||
import { selectCanvasSlice, selectEntityOrThrow } from 'features/controlLayers/store/selectors';
|
||||
import type {
|
||||
CanvasEntityIdentifier,
|
||||
@ -30,7 +31,7 @@ export const useControlLayerControlAdapter = (entityIdentifier: CanvasEntityIden
|
||||
export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig => {
|
||||
const [modelConfigs] = useControlNetAndT2IAdapterModels();
|
||||
|
||||
const baseModel = useAppSelector((s) => s.params.model?.base);
|
||||
const baseModel = useAppSelector(selectBase);
|
||||
|
||||
const defaultControlAdapter = useMemo(() => {
|
||||
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
|
||||
@ -51,7 +52,7 @@ export const useDefaultControlAdapter = (): ControlNetConfig | T2IAdapterConfig
|
||||
export const useDefaultIPAdapter = (): IPAdapterConfig => {
|
||||
const [modelConfigs] = useIPAdapterModels();
|
||||
|
||||
const baseModel = useAppSelector((s) => s.params.model?.base);
|
||||
const baseModel = useAppSelector(selectBase);
|
||||
|
||||
const defaultControlAdapter = useMemo(() => {
|
||||
const compatibleModels = modelConfigs.filter((m) => (baseModel ? m.base === baseModel : true));
|
||||
|
@ -1,4 +1,4 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
import { CLIP_SKIP_MAP } from 'features/parameters/types/constants';
|
||||
@ -271,6 +271,7 @@ export const {
|
||||
} = paramsSlice.actions;
|
||||
|
||||
export const selectParamsSlice = (state: RootState) => state.params;
|
||||
export const selectBase = createSelector(selectParamsSlice, (params) => params.model?.base);
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrate = (state: any): any => {
|
||||
|
@ -39,6 +39,11 @@ export const selectEntityCount = createSelector(selectCanvasSlice, (canvas) => {
|
||||
);
|
||||
});
|
||||
|
||||
/**
|
||||
* Selects if the canvas has any entities.
|
||||
*/
|
||||
export const selectHasEntities = createSelector(selectEntityCount, (count) => count > 0);
|
||||
|
||||
/**
|
||||
* Selects the optimal dimension for the canvas based on the currently-model
|
||||
*/
|
||||
@ -185,4 +190,3 @@ export const selectIsSelectedEntityDrawable = createSelector(
|
||||
return isDrawableEntityType(selectedEntityIdentifier.type);
|
||||
}
|
||||
);
|
||||
|
||||
|
@ -1,5 +1,5 @@
|
||||
import { createSlice, type PayloadAction } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig } from 'app/store/store';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import type { RgbaColor } from 'features/controlLayers/store/types';
|
||||
|
||||
export type ToolState = {
|
||||
@ -52,3 +52,5 @@ export const toolPersistConfig: PersistConfig<ToolState> = {
|
||||
migrate,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
export const selectToolSlice = (state: RootState) => state.tool;
|
||||
|
@ -3,6 +3,7 @@ import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { $isConnected } from 'app/hooks/useSocketIO';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiTrashSimpleBold } from 'react-icons/pi';
|
||||
@ -15,7 +16,7 @@ export const DeleteImageButton = memo((props: DeleteImageButtonProps) => {
|
||||
const { onClick, isDisabled } = props;
|
||||
const { t } = useTranslation();
|
||||
const isConnected = useStore($isConnected);
|
||||
const imageSelectionLength: number = useAppSelector((s) => s.gallery.selection.length);
|
||||
const imageSelectionLength = useAppSelector(selectSelectionCount);
|
||||
const labelMessage: string = `${t('gallery.deleteImage', { count: imageSelectionLength })} (Del)`;
|
||||
|
||||
return (
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { ConfirmationAlertDialog, Divider, Flex, FormControl, FormLabel, Switch, Text } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectCanvasSlice } from 'features/controlLayers/store/selectors';
|
||||
@ -11,7 +12,7 @@ import {
|
||||
} from 'features/deleteImageModal/store/slice';
|
||||
import type { ImageUsage } from 'features/deleteImageModal/store/types';
|
||||
import { selectNodesSlice } from 'features/nodes/store/nodesSlice';
|
||||
import { setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { selectSystemSlice, setShouldConfirmOnDelete } from 'features/system/store/systemSlice';
|
||||
import { some } from 'lodash-es';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
@ -41,11 +42,17 @@ const selectImageUsages = createMemoizedSelector(
|
||||
}
|
||||
);
|
||||
|
||||
const selectShouldConfirmOnDelete = createSelector(selectSystemSlice, (system) => system.shouldConfirmOnDelete);
|
||||
const selectIsModalOpen = createSelector(
|
||||
selectDeleteImageModalSlice,
|
||||
(deleteImageModal) => deleteImageModal.isModalOpen
|
||||
);
|
||||
|
||||
const DeleteImageModal = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const shouldConfirmOnDelete = useAppSelector((s) => s.system.shouldConfirmOnDelete);
|
||||
const isModalOpen = useAppSelector((s) => s.deleteImageModal.isModalOpen);
|
||||
const shouldConfirmOnDelete = useAppSelector(selectShouldConfirmOnDelete);
|
||||
const isModalOpen = useAppSelector(selectIsModalOpen);
|
||||
const { imagesToDelete, imagesUsage, imageUsageSummary } = useAppSelector(selectImageUsages);
|
||||
|
||||
const handleChangeShouldConfirmOnDelete = useCallback(
|
||||
|
@ -2,6 +2,7 @@ import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Heading, Image, Text } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import type { TypesafeDraggableData } from 'features/dnd/types';
|
||||
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
@ -34,7 +35,7 @@ const multiImageStyles: ChakraProps['sx'] = {
|
||||
|
||||
const DragPreview = (props: OverlayDragImageProps) => {
|
||||
const { t } = useTranslation();
|
||||
const selectionCount = useAppSelector((s) => s.gallery.selection.length);
|
||||
const selectionCount = useAppSelector(selectSelectionCount);
|
||||
if (!props.dragData) {
|
||||
return null;
|
||||
}
|
||||
|
@ -1,18 +1,20 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { maxPromptsChanged } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { maxPromptsChanged, selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectMaxPrompts = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.maxPrompts);
|
||||
const selectMaxPromptsConfig = createSelector(selectConfigSlice, (config) => config.sd.dynamicPrompts.maxPrompts);
|
||||
const selectIsDisabled = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => !dynamicPrompts.combinatorial);
|
||||
|
||||
const ParamDynamicPromptsMaxPrompts = () => {
|
||||
const maxPrompts = useAppSelector((s) => s.dynamicPrompts.maxPrompts);
|
||||
const sliderMin = useAppSelector((s) => s.config.sd.dynamicPrompts.maxPrompts.sliderMin);
|
||||
const sliderMax = useAppSelector((s) => s.config.sd.dynamicPrompts.maxPrompts.sliderMax);
|
||||
const numberInputMin = useAppSelector((s) => s.config.sd.dynamicPrompts.maxPrompts.numberInputMin);
|
||||
const numberInputMax = useAppSelector((s) => s.config.sd.dynamicPrompts.maxPrompts.numberInputMax);
|
||||
const initial = useAppSelector((s) => s.config.sd.dynamicPrompts.maxPrompts.initial);
|
||||
const isDisabled = useAppSelector((s) => !s.dynamicPrompts.combinatorial);
|
||||
const maxPrompts = useAppSelector(selectMaxPrompts);
|
||||
const config = useAppSelector(selectMaxPromptsConfig);
|
||||
const isDisabled = useAppSelector(selectIsDisabled);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -29,18 +31,18 @@ const ParamDynamicPromptsMaxPrompts = () => {
|
||||
<FormLabel>{t('dynamicPrompts.maxPrompts')}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<CompositeSlider
|
||||
min={sliderMin}
|
||||
max={sliderMax}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
value={maxPrompts}
|
||||
defaultValue={initial}
|
||||
defaultValue={config.initial}
|
||||
onChange={handleChange}
|
||||
marks
|
||||
/>
|
||||
<CompositeNumberInput
|
||||
min={numberInputMin}
|
||||
max={numberInputMax}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
value={maxPrompts}
|
||||
defaultValue={initial}
|
||||
defaultValue={config.initial}
|
||||
onChange={handleChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { Flex, FormControl, FormLabel, ListItem, OrderedList, Spinner, Text } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
@ -10,17 +10,20 @@ import { memo, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiWarningCircleBold } from 'react-icons/pi';
|
||||
|
||||
const selectPrompts = createMemoizedSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.prompts);
|
||||
|
||||
const listItemStyles: ChakraProps['sx'] = {
|
||||
'&::marker': { color: 'base.500' },
|
||||
};
|
||||
|
||||
const selectPrompts = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.prompts);
|
||||
const selectParsingError = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.parsingError);
|
||||
const selectIsError = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.isError);
|
||||
const selectIsLoading = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.isLoading);
|
||||
|
||||
const ParamDynamicPromptsPreview = () => {
|
||||
const { t } = useTranslation();
|
||||
const parsingError = useAppSelector((s) => s.dynamicPrompts.parsingError);
|
||||
const isError = useAppSelector((s) => s.dynamicPrompts.isError);
|
||||
const isLoading = useAppSelector((s) => s.dynamicPrompts.isLoading);
|
||||
const parsingError = useAppSelector(selectParsingError);
|
||||
const isError = useAppSelector(selectIsError);
|
||||
const isLoading = useAppSelector(selectIsLoading);
|
||||
const prompts = useAppSelector(selectPrompts);
|
||||
|
||||
const label = useMemo(() => {
|
||||
|
@ -1,15 +1,22 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { isSeedBehaviour, seedBehaviourChanged } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import {
|
||||
isSeedBehaviour,
|
||||
seedBehaviourChanged,
|
||||
selectDynamicPromptsSlice,
|
||||
} from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectSeedBehaviour = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.seedBehaviour);
|
||||
|
||||
const ParamDynamicPromptsSeedBehaviour = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const seedBehaviour = useAppSelector((s) => s.dynamicPrompts.seedBehaviour);
|
||||
const seedBehaviour = useAppSelector(selectSeedBehaviour);
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(() => {
|
||||
return [
|
||||
|
@ -1,7 +1,9 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { IconButton, spinAnimation, Tooltip } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useDynamicPromptsModal } from 'features/dynamicPrompts/hooks/useDynamicPromptsModal';
|
||||
import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { BsBracesAsterisk } from 'react-icons/bs';
|
||||
@ -10,10 +12,15 @@ const loadingStyles: SystemStyleObject = {
|
||||
svg: { animation: spinAnimation },
|
||||
};
|
||||
|
||||
const selectIsError = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) =>
|
||||
Boolean(dynamicPrompts.isError || dynamicPrompts.parsingError)
|
||||
);
|
||||
const selectIsLoading = createSelector(selectDynamicPromptsSlice, (dynamicPrompts) => dynamicPrompts.isLoading);
|
||||
|
||||
export const ShowDynamicPromptsPreviewButton = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const isLoading = useAppSelector((s) => s.dynamicPrompts.isLoading);
|
||||
const isError = useAppSelector((s) => Boolean(s.dynamicPrompts.isError || s.dynamicPrompts.parsingError));
|
||||
const isLoading = useAppSelector(selectIsLoading);
|
||||
const isError = useAppSelector(selectIsError);
|
||||
const { isOpen, onOpen } = useDynamicPromptsModal();
|
||||
|
||||
return (
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectAutoAddBoardId, selectAutoAssignBoardOnClick } from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -9,8 +10,8 @@ import { useListAllBoardsQuery } from 'services/api/endpoints/boards';
|
||||
const BoardAutoAddSelect = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const { options, hasBoards } = useListAllBoardsQuery(
|
||||
{},
|
||||
{
|
||||
|
@ -2,7 +2,8 @@ import type { ContextMenuProps } from '@invoke-ai/ui-library';
|
||||
import { ContextMenu, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { autoAddBoardIdChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { selectAutoAddBoardId, selectAutoAssignBoardOnClick } from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
@ -24,9 +25,9 @@ type Props = {
|
||||
const BoardContextMenu = ({ board, setBoardToDelete, children }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const selectIsSelectedForAutoAdd = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => board.board_id === gallery.autoAddBoardId),
|
||||
() => createSelector(selectAutoAddBoardId, (autoAddBoardId) => board.board_id === autoAddBoardId),
|
||||
[board.board_id]
|
||||
);
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { boardIdSelected, boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { selectAllowPrivateBoards } from 'features/system/store/configSelectors';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiPlusBold } from 'react-icons/pi';
|
||||
@ -13,7 +14,7 @@ type Props = {
|
||||
const AddBoardButton = ({ isPrivateBoard }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||
const allowPrivateBoards = useAppSelector(selectAllowPrivateBoards);
|
||||
const [createBoard, { isLoading }] = useCreateBoardMutation();
|
||||
const label = useMemo(() => {
|
||||
if (!allowPrivateBoards) {
|
||||
|
@ -1,7 +1,12 @@
|
||||
import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library';
|
||||
import { EMPTY_ARRAY } from 'app/store/constants';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectListBoardsQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
selectBoardSearchText,
|
||||
selectListBoardsQueryArgs,
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { selectAllowPrivateBoards } from 'features/system/store/configSelectors';
|
||||
import { useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiCaretDownBold } from 'react-icons/pi';
|
||||
@ -17,13 +22,14 @@ type Props = {
|
||||
setBoardToDelete: (board?: BoardDTO) => void;
|
||||
};
|
||||
|
||||
|
||||
export const BoardsList = ({ isPrivate, setBoardToDelete }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const queryArgs = useAppSelector(selectListBoardsQueryArgs);
|
||||
const { data: boards } = useListAllBoardsQuery(queryArgs);
|
||||
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||
const allowPrivateBoards = useAppSelector(selectAllowPrivateBoards);
|
||||
const { isOpen, onToggle } = useDisclosure({ defaultIsOpen: true });
|
||||
|
||||
const filteredBoards = useMemo(() => {
|
||||
|
@ -2,6 +2,7 @@ import { Box } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants';
|
||||
import DeleteBoardModal from 'features/gallery/components/Boards/DeleteBoardModal';
|
||||
import { selectAllowPrivateBoards } from 'features/system/store/configSelectors';
|
||||
import { OverlayScrollbarsComponent } from 'overlayscrollbars-react';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useState } from 'react';
|
||||
@ -15,7 +16,7 @@ const overlayScrollbarsStyles: CSSProperties = {
|
||||
};
|
||||
|
||||
const BoardsListWrapper = () => {
|
||||
const allowPrivateBoards = useAppSelector((s) => s.config.allowPrivateBoards);
|
||||
const allowPrivateBoards = useAppSelector(selectAllowPrivateBoards);
|
||||
const [boardToDelete, setBoardToDelete] = useState<BoardDTO>();
|
||||
|
||||
return (
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent, KeyboardEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
@ -8,7 +9,7 @@ import { PiXBold } from 'react-icons/pi';
|
||||
|
||||
const BoardsSearch = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const handleBoardSearch = useCallback(
|
||||
|
@ -18,6 +18,11 @@ import type { AddToBoardDropData } from 'features/dnd/types';
|
||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||
import BoardContextMenu from 'features/gallery/components/Boards/BoardContextMenu';
|
||||
import { BoardTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTooltip';
|
||||
import {
|
||||
selectAutoAddBoardId,
|
||||
selectAutoAssignBoardOnClick,
|
||||
selectSelectedBoardId,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import type { MouseEvent, MouseEventHandler, MutableRefObject } from 'react';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
@ -49,9 +54,9 @@ interface GalleryBoardProps {
|
||||
const GalleryBoard = ({ board, isSelected, setBoardToDelete }: GalleryBoardProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const editingDisclosure = useDisclosure();
|
||||
const [localBoardName, setLocalBoardName] = useState(board.board_name);
|
||||
const onStartEditingRef = useRef<MouseEventHandler | undefined>(undefined);
|
||||
|
@ -6,6 +6,11 @@ import type { RemoveFromBoardDropData } from 'features/dnd/types';
|
||||
import { AutoAddBadge } from 'features/gallery/components/Boards/AutoAddBadge';
|
||||
import { BoardTooltip } from 'features/gallery/components/Boards/BoardsList/BoardTooltip';
|
||||
import NoBoardBoardContextMenu from 'features/gallery/components/Boards/NoBoardBoardContextMenu';
|
||||
import {
|
||||
selectAutoAddBoardId,
|
||||
selectAutoAssignBoardOnClick,
|
||||
selectBoardSearchText,
|
||||
} from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged, boardIdSelected } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -27,9 +32,9 @@ const NoBoardBoard = memo(({ isSelected }: Props) => {
|
||||
return { imagesTotal: data?.total ?? 0 };
|
||||
},
|
||||
});
|
||||
const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||
const autoAddBoardId = useAppSelector(selectAutoAddBoardId);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const boardName = useBoardName('none');
|
||||
const handleSelectBoard = useCallback(() => {
|
||||
dispatch(boardIdSelected({ boardId: 'none' }));
|
||||
|
@ -1,6 +1,8 @@
|
||||
import type { ContextMenuProps } from '@invoke-ai/ui-library';
|
||||
import { ContextMenu, MenuGroup, MenuItem, MenuList } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectAutoAddBoardId, selectAutoAssignBoardOnClick } from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAddBoardIdChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo, useCallback } from 'react';
|
||||
@ -12,11 +14,13 @@ type Props = {
|
||||
children: ContextMenuProps<HTMLDivElement>['children'];
|
||||
};
|
||||
|
||||
const selectIsSelectedForAutoAdd = createSelector(selectAutoAddBoardId, (autoAddBoardId) => autoAddBoardId === 'none');
|
||||
|
||||
const NoBoardBoardContextMenu = ({ children }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const isSelectedForAutoAdd = useAppSelector((s) => s.gallery.autoAddBoardId === 'none');
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
const isSelectedForAutoAdd = useAppSelector(selectIsSelectedForAutoAdd);
|
||||
const isBulkDownloadEnabled = useFeatureStatus('bulkDownload');
|
||||
|
||||
const [bulkDownload] = useBulkDownloadImagesMutation();
|
||||
|
@ -11,9 +11,11 @@ import {
|
||||
Text,
|
||||
useDisclosure,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useGallerySearchTerm } from 'features/gallery/components/ImageGrid/useGallerySearchTerm';
|
||||
import { galleryViewChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import { galleryViewChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -38,11 +40,14 @@ const SELECTED_STYLES: ChakraProps['sx'] = {
|
||||
|
||||
const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
|
||||
const selectGalleryView = createSelector(selectGallerySlice, (gallery) => gallery.galleryView);
|
||||
const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
|
||||
|
||||
export const Gallery = () => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const galleryView = useAppSelector((s) => s.gallery.galleryView);
|
||||
const initialSearchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
||||
const galleryView = useAppSelector(selectGalleryView);
|
||||
const initialSearchTerm = useAppSelector(selectSearchTerm);
|
||||
const searchDisclosure = useDisclosure({ defaultIsOpen: initialSearchTerm.length > 0 });
|
||||
const [searchTerm, onChangeSearchTerm, onResetSearchTerm] = useGallerySearchTerm();
|
||||
|
||||
@ -59,7 +64,7 @@ export const Gallery = () => {
|
||||
onResetSearchTerm();
|
||||
}, [onResetSearchTerm, searchDisclosure]);
|
||||
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const boardName = useBoardName(selectedBoardId);
|
||||
|
||||
return (
|
||||
|
@ -2,6 +2,7 @@ import { Box, Button, Collapse, Divider, Flex, IconButton, useDisclosure } from
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { GalleryHeader } from 'features/gallery/components/GalleryHeader';
|
||||
import { selectBoardSearchText } from 'features/gallery/store/gallerySelectors';
|
||||
import { boardSearchTextChanged } from 'features/gallery/store/gallerySlice';
|
||||
import ResizeHandle from 'features/ui/components/tabs/ResizeHandle';
|
||||
import { usePanel, type UsePanelOptions } from 'features/ui/hooks/usePanel';
|
||||
@ -21,7 +22,7 @@ const COLLAPSE_STYLES: CSSProperties = { flexShrink: 0, minHeight: 0 };
|
||||
|
||||
const GalleryPanelContent = () => {
|
||||
const { t } = useTranslation();
|
||||
const boardSearchText = useAppSelector((s) => s.gallery.boardSearchText);
|
||||
const boardSearchText = useAppSelector(selectBoardSearchText);
|
||||
const dispatch = useAppDispatch();
|
||||
const boardSearchDisclosure = useDisclosure({ defaultIsOpen: !!boardSearchText.length });
|
||||
const panelGroupRef = useRef<ImperativePanelGroupHandle>(null);
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { alwaysShowImageSizeBadgeChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { alwaysShowImageSizeBadgeChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectAlwaysShowImageSizeBadge = createSelector(
|
||||
selectGallerySlice,
|
||||
(gallery) => gallery.alwaysShowImageSizeBadge
|
||||
);
|
||||
|
||||
const GallerySettingsPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
|
||||
const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShowImageSizeBadge);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(alwaysShowImageSizeBadgeChanged(e.target.checked)),
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectAutoAssignBoardOnClick } from 'features/gallery/store/gallerySelectors';
|
||||
import { autoAssignBoardOnClickChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
@ -8,7 +9,7 @@ import { useTranslation } from 'react-i18next';
|
||||
const GallerySettingsPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const autoAssignBoardOnClick = useAppSelector((s) => s.gallery.autoAssignBoardOnClick);
|
||||
const autoAssignBoardOnClick = useAppSelector(selectAutoAssignBoardOnClick);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(autoAssignBoardOnClickChanged(e.target.checked)),
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { selectGallerySlice, shouldAutoSwitchChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectShouldAutoSwitch = createSelector(selectGallerySlice, (gallery) => gallery.shouldAutoSwitch);
|
||||
|
||||
const GallerySettingsPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const shouldAutoSwitch = useAppSelector((s) => s.gallery.shouldAutoSwitch);
|
||||
const shouldAutoSwitch = useAppSelector(selectShouldAutoSwitch);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectGalleryImageMinimumWidth } from 'features/gallery/store/gallerySelectors';
|
||||
import { setGalleryImageMinimumWidth } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -7,7 +8,7 @@ import { useTranslation } from 'react-i18next';
|
||||
const GallerySettingsPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const galleryImageMinimumWidth = useAppSelector((s) => s.gallery.galleryImageMinimumWidth);
|
||||
const galleryImageMinimumWidth = useAppSelector(selectGalleryImageMinimumWidth);
|
||||
|
||||
const onChange = useCallback(
|
||||
(v: number) => {
|
||||
|
@ -1,14 +1,20 @@
|
||||
import { Checkbox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { shouldShowArchivedBoardsChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { selectGallerySlice, shouldShowArchivedBoardsChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectShouldShowArchivedBoards = createSelector(
|
||||
selectGallerySlice,
|
||||
(gallery) => gallery.shouldShowArchivedBoards
|
||||
);
|
||||
|
||||
const GallerySettingsPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const shouldShowArchivedBoards = useAppSelector((s) => s.gallery.shouldShowArchivedBoards);
|
||||
const shouldShowArchivedBoards = useAppSelector(selectShouldShowArchivedBoards);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -1,14 +1,17 @@
|
||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { starredFirstChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { selectGallerySlice, starredFirstChanged } from 'features/gallery/store/gallerySlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectStarredFirst = createSelector(selectGallerySlice, (gallery) => gallery.starredFirst);
|
||||
|
||||
const GallerySettingsPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const starredFirst = useAppSelector((s) => s.gallery.starredFirst);
|
||||
const starredFirst = useAppSelector(selectStarredFirst);
|
||||
|
||||
const onChange = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => {
|
||||
|
@ -1,16 +1,19 @@
|
||||
import type { ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import type { SingleValue } from 'chakra-react-select';
|
||||
import { orderDirChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { orderDirChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { assert } from 'tsafe';
|
||||
|
||||
const selectOrderDir = createSelector(selectGallerySlice, (gallery) => gallery.orderDir);
|
||||
|
||||
const GallerySettingsPopover = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const orderDir = useAppSelector((s) => s.gallery.orderDir);
|
||||
const orderDir = useAppSelector(selectOrderDir);
|
||||
|
||||
const options = useMemo<ComboboxOption[]>(
|
||||
() => [
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { ContextMenuProps } from '@invoke-ai/ui-library';
|
||||
import { ContextMenu, MenuList } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSelectionCount } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useCallback } from 'react';
|
||||
import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
@ -13,7 +14,7 @@ type Props = {
|
||||
};
|
||||
|
||||
const ImageContextMenu = ({ imageDTO, children }: Props) => {
|
||||
const selectionCount = useAppSelector((s) => s.gallery.selection.length);
|
||||
const selectionCount = useAppSelector(selectSelectionCount);
|
||||
|
||||
const renderMenuFunc = useCallback(() => {
|
||||
if (!imageDTO) {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { Flex, MenuDivider, MenuItem, Spinner } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useCopyImageToClipboard } from 'common/hooks/useCopyImageToClipboard';
|
||||
@ -8,14 +9,14 @@ import { imagesToChangeSelected, isModalOpenChanged } from 'features/changeBoard
|
||||
import { imagesToDeleteSelected } from 'features/deleteImageModal/store/slice';
|
||||
import { useImageActions } from 'features/gallery/hooks/useImageActions';
|
||||
import { sentImageToCanvas, sentImageToImg2Img } from 'features/gallery/store/actions';
|
||||
import { imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { imageToCompareChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { $templates } from 'features/nodes/store/nodesSlice';
|
||||
import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { setActiveTab } from 'features/ui/store/uiSlice';
|
||||
import { useGetAndLoadEmbeddedWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadEmbeddedWorkflow';
|
||||
import { size } from 'lodash-es';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import {
|
||||
PiArrowsCounterClockwiseBold,
|
||||
@ -42,7 +43,11 @@ type SingleSelectionMenuItemsProps = {
|
||||
|
||||
const SingleSelectionMenuItems = (props: SingleSelectionMenuItemsProps) => {
|
||||
const { imageDTO } = props;
|
||||
const maySelectForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name !== imageDTO.image_name);
|
||||
const selectMaySelectForCompare = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name !== imageDTO.image_name),
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const maySelectForCompare = useAppSelector(selectMaySelectForCompare);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const customStarUi = useStore($customStarUI);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Box, Flex, Text, useShiftModifier } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { $customStarUI } from 'app/store/nanostores/customStarUI';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
@ -11,7 +12,12 @@ import type { GallerySelectionDraggableData, ImageDraggableData, TypesafeDraggab
|
||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||
import { useMultiselect } from 'features/gallery/hooks/useMultiselect';
|
||||
import { useScrollIntoView } from 'features/gallery/hooks/useScrollIntoView';
|
||||
import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { selectSelectedBoardId } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectGallerySlice,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import type { MouseEvent, MouseEventHandler } from 'react';
|
||||
import { memo, useCallback, useMemo, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -38,11 +44,20 @@ interface HoverableImageProps {
|
||||
index: number;
|
||||
}
|
||||
|
||||
const selectAlwaysShouldImageSizeBadge = createSelector(
|
||||
selectGallerySlice,
|
||||
(gallery) => gallery.alwaysShowImageSizeBadge
|
||||
);
|
||||
|
||||
const GalleryImage = ({ index, imageDTO }: HoverableImageProps) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const selectedBoardId = useAppSelector((s) => s.gallery.selectedBoardId);
|
||||
const alwaysShowImageSizeBadge = useAppSelector((s) => s.gallery.alwaysShowImageSizeBadge);
|
||||
const isSelectedForCompare = useAppSelector((s) => s.gallery.imageToCompare?.image_name === imageDTO.image_name);
|
||||
const selectedBoardId = useAppSelector(selectSelectedBoardId);
|
||||
const selectIsSelectedForCompare = useMemo(
|
||||
() => createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare?.image_name === imageDTO.image_name),
|
||||
[imageDTO.image_name]
|
||||
);
|
||||
const alwaysShowImageSizeBadge = useAppSelector(selectAlwaysShouldImageSizeBadge);
|
||||
const isSelectedForCompare = useAppSelector(selectIsSelectedForCompare);
|
||||
const { handleClick, isSelected, areMultiplesSelected } = useMultiselect(imageDTO);
|
||||
|
||||
const customStarUi = useStore($customStarUI);
|
||||
|
@ -4,7 +4,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import { GallerySelectionCountTag } from 'features/gallery/components/ImageGrid/GallerySelectionCountTag';
|
||||
import { useGalleryHotkeys } from 'features/gallery/hooks/useGalleryHotkeys';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectGalleryImageMinimumWidth, selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { limitChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { memo, useEffect, useMemo, useState } from 'react';
|
||||
@ -59,7 +59,7 @@ export default memo(GalleryImageGrid);
|
||||
|
||||
const Content = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const galleryImageMinimumWidth = useAppSelector((s) => s.gallery.galleryImageMinimumWidth);
|
||||
const galleryImageMinimumWidth = useAppSelector(selectGalleryImageMinimumWidth);
|
||||
|
||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
||||
const { imageDTOs } = useListImagesQuery(queryArgs, {
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAssertSingleton } from 'common/hooks/useAssertSingleton';
|
||||
import { selectSearchTerm } from 'features/gallery/store/gallerySelectors';
|
||||
import { searchTermChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { debounce } from 'lodash-es';
|
||||
import { useCallback, useMemo, useState } from 'react';
|
||||
@ -9,7 +10,7 @@ export const useGallerySearchTerm = () => {
|
||||
useAssertSingleton('gallery-search-state');
|
||||
|
||||
const dispatch = useAppDispatch();
|
||||
const searchTerm = useAppSelector((s) => s.gallery.searchTerm);
|
||||
const searchTerm = useAppSelector(selectSearchTerm);
|
||||
|
||||
const [localSearchTerm, setLocalSearchTerm] = useState(searchTerm);
|
||||
|
||||
|
@ -10,6 +10,7 @@ import {
|
||||
UnorderedList,
|
||||
} from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectComparisonFit, selectComparisonMode } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
comparedImagesSwapped,
|
||||
comparisonFitChanged,
|
||||
@ -25,8 +26,8 @@ import { PiArrowsOutBold, PiQuestion, PiSwapBold, PiXBold } from 'react-icons/pi
|
||||
export const CompareToolbar = memo(() => {
|
||||
const { t } = useTranslation();
|
||||
const dispatch = useAppDispatch();
|
||||
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
|
||||
const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit);
|
||||
const comparisonMode = useAppSelector(selectComparisonMode);
|
||||
const comparisonFit = useAppSelector(selectComparisonFit);
|
||||
const setComparisonModeSlider = useCallback(() => {
|
||||
dispatch(comparisonModeChanged('slider'));
|
||||
}, [dispatch]);
|
||||
|
@ -1,6 +1,5 @@
|
||||
import { Box, Flex } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
@ -8,7 +7,8 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import type { TypesafeDraggableData } from 'features/dnd/types';
|
||||
import ImageMetadataViewer from 'features/gallery/components/ImageMetadataViewer/ImageMetadataViewer';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectLastSelectedImageName } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectShouldShowImageDetails, selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
import { AnimatePresence, motion } from 'framer-motion';
|
||||
import { memo, useCallback, useMemo, useRef, useState } from 'react';
|
||||
@ -19,17 +19,12 @@ import { $hasProgress } from 'services/events/setEventListeners';
|
||||
|
||||
import ProgressImage from './ProgressImage';
|
||||
|
||||
const selectLastSelectedImageName = createSelector(
|
||||
selectLastSelectedImage,
|
||||
(lastSelectedImage) => lastSelectedImage?.image_name
|
||||
);
|
||||
|
||||
const CurrentImagePreview = () => {
|
||||
const { t } = useTranslation();
|
||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
|
||||
const imageName = useAppSelector(selectLastSelectedImageName);
|
||||
const hasDenoiseProgress = useStore($hasProgress);
|
||||
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
|
||||
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
|
||||
|
||||
const { currentData: imageDTO } = useGetImageDTOQuery(imageName ?? skipToken);
|
||||
|
||||
|
@ -5,6 +5,7 @@ import { selectComparisonImages } from 'features/gallery/components/ImageViewer/
|
||||
import { ImageComparisonHover } from 'features/gallery/components/ImageViewer/ImageComparisonHover';
|
||||
import { ImageComparisonSideBySide } from 'features/gallery/components/ImageViewer/ImageComparisonSideBySide';
|
||||
import { ImageComparisonSlider } from 'features/gallery/components/ImageViewer/ImageComparisonSlider';
|
||||
import { selectComparisonMode } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PiImagesBold } from 'react-icons/pi';
|
||||
@ -15,7 +16,7 @@ type Props = {
|
||||
|
||||
export const ImageComparison = memo(({ containerDims }: Props) => {
|
||||
const { t } = useTranslation();
|
||||
const comparisonMode = useAppSelector((s) => s.gallery.comparisonMode);
|
||||
const comparisonMode = useAppSelector(selectComparisonMode);
|
||||
const { firstImage, secondImage } = useAppSelector(selectComparisonImages);
|
||||
|
||||
if (!firstImage || !secondImage) {
|
||||
|
@ -5,13 +5,14 @@ import { preventDefault } from 'common/util/stopPropagation';
|
||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||
import type { Dimensions } from 'features/controlLayers/store/types';
|
||||
import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
|
||||
import { selectComparisonFit } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
|
||||
import type { ComparisonProps } from './common';
|
||||
import { fitDimsToContainer, getSecondImageDims } from './common';
|
||||
|
||||
export const ImageComparisonHover = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => {
|
||||
const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit);
|
||||
const comparisonFit = useAppSelector(selectComparisonFit);
|
||||
const imageContainerRef = useRef<HTMLDivElement>(null);
|
||||
const mouseOver = useBoolean(false);
|
||||
const fittedDims = useMemo<Dimensions>(
|
||||
|
@ -4,6 +4,7 @@ import { preventDefault } from 'common/util/stopPropagation';
|
||||
import { TRANSPARENCY_CHECKER_PATTERN } from 'features/controlLayers/konva/constants';
|
||||
import type { Dimensions } from 'features/controlLayers/store/types';
|
||||
import { ImageComparisonLabel } from 'features/gallery/components/ImageViewer/ImageComparisonLabel';
|
||||
import { selectComparisonFit } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useCallback, useEffect, useMemo, useRef, useState } from 'react';
|
||||
import { PiCaretLeftBold, PiCaretRightBold } from 'react-icons/pi';
|
||||
|
||||
@ -19,7 +20,7 @@ const HANDLE_INNER_LEFT_PX = `${HANDLE_HITBOX / 2 - HANDLE_WIDTH / 2}px`;
|
||||
const HANDLE_LEFT_INITIAL_PX = `calc(${INITIAL_POS} - ${HANDLE_HITBOX / 2}px)`;
|
||||
|
||||
export const ImageComparisonSlider = memo(({ firstImage, secondImage, containerDims }: ComparisonProps) => {
|
||||
const comparisonFit = useAppSelector((s) => s.gallery.comparisonFit);
|
||||
const comparisonFit = useAppSelector(selectComparisonFit);
|
||||
// How far the handle is from the left - this will be a CSS calculation that takes into account the handle width
|
||||
const [left, setLeft] = useState(HANDLE_LEFT_INITIAL_PX);
|
||||
// How wide the first image is
|
||||
|
@ -6,11 +6,12 @@ import CurrentImagePreview from 'features/gallery/components/ImageViewer/Current
|
||||
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 { selectHasImageToCompare } from 'features/gallery/store/gallerySelectors';
|
||||
import { memo, useRef } from 'react';
|
||||
import { useMeasure } from 'react-use';
|
||||
|
||||
export const ImageViewer = memo(() => {
|
||||
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
|
||||
const hasImageToCompare = useAppSelector(selectHasImageToCompare);
|
||||
const [containerRef, containerDims] = useMeasure<HTMLDivElement>();
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('imageViewer', ref);
|
||||
@ -33,11 +34,11 @@ export const ImageViewer = memo(() => {
|
||||
alignItems="center"
|
||||
justifyContent="center"
|
||||
>
|
||||
{isComparing && <CompareToolbar />}
|
||||
{!isComparing && <ViewerToolbar />}
|
||||
{hasImageToCompare && <CompareToolbar />}
|
||||
{!hasImageToCompare && <ViewerToolbar />}
|
||||
<Box ref={containerRef} w="full" h="full">
|
||||
{!isComparing && <CurrentImagePreview />}
|
||||
{isComparing && <ImageComparison containerDims={containerDims} />}
|
||||
{!hasImageToCompare && <CurrentImagePreview />}
|
||||
{hasImageToCompare && <ImageComparison containerDims={containerDims} />}
|
||||
</Box>
|
||||
<ImageComparisonDroppable />
|
||||
</Flex>
|
||||
|
@ -1,13 +1,20 @@
|
||||
import type { SystemStyleObject } from '@invoke-ai/ui-library';
|
||||
import { Image } from '@invoke-ai/ui-library';
|
||||
import { useStore } from '@nanostores/react';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectSystemSlice } from 'features/system/store/systemSlice';
|
||||
import { memo, useMemo } from 'react';
|
||||
import { $progressImage } from 'services/events/setEventListeners';
|
||||
|
||||
const selectShouldAntialiasProgressImage = createSelector(
|
||||
selectSystemSlice,
|
||||
(system) => system.shouldAntialiasProgressImage
|
||||
);
|
||||
|
||||
const CurrentImagePreview = () => {
|
||||
const progressImage = useStore($progressImage);
|
||||
const shouldAntialiasProgressImage = useAppSelector((s) => s.system.shouldAntialiasProgressImage);
|
||||
const shouldAntialiasProgressImage = useAppSelector(selectShouldAntialiasProgressImage);
|
||||
|
||||
const sx = useMemo<SystemStyleObject>(
|
||||
() => ({
|
||||
|
@ -2,6 +2,7 @@ import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectShouldShowImageDetails } from 'features/ui/store/uiSelectors';
|
||||
import { setShouldShowImageDetails } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useHotkeys } from 'react-hotkeys-hook';
|
||||
@ -11,7 +12,7 @@ import { useGetImageDTOQuery } from 'services/api/endpoints/images';
|
||||
|
||||
export const ToggleMetadataViewerButton = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldShowImageDetails = useAppSelector((s) => s.ui.shouldShowImageDetails);
|
||||
const shouldShowImageDetails = useAppSelector(selectShouldShowImageDetails);
|
||||
const lastSelectedImage = useAppSelector(selectLastSelectedImage);
|
||||
const { t } = useTranslation();
|
||||
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { IconButton } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors';
|
||||
import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -7,7 +8,7 @@ import { PiHourglassHighBold } from 'react-icons/pi';
|
||||
|
||||
export const ToggleProgressButton = memo(() => {
|
||||
const dispatch = useAppDispatch();
|
||||
const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer);
|
||||
const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer);
|
||||
const { t } = useTranslation();
|
||||
|
||||
const onClick = useCallback(() => {
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { ToggleMetadataViewerButton } from 'features/gallery/components/ImageViewer/ToggleMetadataViewerButton';
|
||||
import { ToggleProgressButton } from 'features/gallery/components/ImageViewer/ToggleProgressButton';
|
||||
@ -8,14 +9,15 @@ import { memo } from 'react';
|
||||
import CurrentImageButtons from './CurrentImageButtons';
|
||||
import { ViewerToggleMenu } from './ViewerToggleMenu';
|
||||
|
||||
const selectShowToggle = createSelector(selectActiveTab, (tab) => {
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
|
||||
export const ViewerToolbar = memo(() => {
|
||||
const showToggle = useAppSelector((s) => {
|
||||
const tab = selectActiveTab(s);
|
||||
if (tab === 'upscaling' || tab === 'workflows') {
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
const showToggle = useAppSelector(selectShowToggle);
|
||||
return (
|
||||
<Flex w="full" gap={2}>
|
||||
<Flex flex={1} justifyContent="center">
|
||||
|
@ -1,35 +1,47 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { imageToCompareChanged, isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { selectHasImageToCompare, selectIsImageViewerOpen } from 'features/gallery/store/gallerySelectors';
|
||||
import {
|
||||
imageToCompareChanged,
|
||||
isImageViewerOpenChanged,
|
||||
selectGallerySlice,
|
||||
} from 'features/gallery/store/gallerySlice';
|
||||
import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice';
|
||||
import { selectUiSlice } from 'features/ui/store/uiSlice';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
const selectIsOpen = createSelector(selectUiSlice, selectWorkflowSlice, selectGallerySlice, (ui, workflow, gallery) => {
|
||||
const tab = ui.activeTab;
|
||||
const workflowsMode = 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 gallery.isImageViewerOpen;
|
||||
});
|
||||
|
||||
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;
|
||||
});
|
||||
const isOpen = useAppSelector(selectIsOpen);
|
||||
return isOpen;
|
||||
};
|
||||
|
||||
const selectIsForcedOpen = createSelector(selectUiSlice, selectWorkflowSlice, (ui, workflow) => {
|
||||
return ui.activeTab === 'upscaling' || (ui.activeTab === 'workflows' && workflow.mode === 'view');
|
||||
});
|
||||
|
||||
export const useImageViewer = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const isComparing = useAppSelector((s) => s.gallery.imageToCompare !== null);
|
||||
const isNaturallyOpen = useAppSelector((s) => s.gallery.isImageViewerOpen);
|
||||
const isForcedOpen = useAppSelector(
|
||||
(s) => s.ui.activeTab === 'upscaling' || (s.ui.activeTab === 'workflows' && s.workflow.mode === 'view')
|
||||
);
|
||||
const isComparing = useAppSelector(selectHasImageToCompare);
|
||||
const isNaturallyOpen = useAppSelector(selectIsImageViewerOpen);
|
||||
const isForcedOpen = useAppSelector(selectIsForcedOpen);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
if (isForcedOpen) {
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { useAltModifier } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { GALLERY_GRID_CLASS_NAME } from 'features/gallery/components/ImageGrid/constants';
|
||||
import { GALLERY_IMAGE_CLASS_NAME } from 'features/gallery/components/ImageGrid/GalleryImage';
|
||||
import { getGalleryImageDataTestId } from 'features/gallery/components/ImageGrid/getGalleryImageDataTestId';
|
||||
import { virtuosoGridRefs } from 'features/gallery/components/ImageGrid/types';
|
||||
import { useGalleryImages } from 'features/gallery/hooks/useGalleryImages';
|
||||
import { selectImageToCompare, selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import { imageSelected, imageToCompareChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { getIsVisible } from 'features/gallery/util/getIsVisible';
|
||||
import { getScrollToIndexAlign } from 'features/gallery/util/getScrollToIndexAlign';
|
||||
@ -127,14 +129,18 @@ type UseGalleryNavigationReturn = {
|
||||
export const useGalleryNavigation = (): UseGalleryNavigationReturn => {
|
||||
const dispatch = useAppDispatch();
|
||||
const alt = useAltModifier();
|
||||
const lastSelectedImage = useAppSelector((s) => {
|
||||
const lastSelected = s.gallery.selection.slice(-1)[0] ?? null;
|
||||
if (alt) {
|
||||
return s.gallery.imageToCompare ?? lastSelected;
|
||||
} else {
|
||||
return lastSelected;
|
||||
}
|
||||
});
|
||||
const selectImage = useMemo(
|
||||
() =>
|
||||
createSelector(selectLastSelectedImage, selectImageToCompare, (lastSelectedImage, imageToCompare) => {
|
||||
if (alt) {
|
||||
return imageToCompare ?? lastSelectedImage;
|
||||
} else {
|
||||
return lastSelectedImage;
|
||||
}
|
||||
}),
|
||||
[alt]
|
||||
);
|
||||
const lastSelectedImage = useAppSelector(selectImage);
|
||||
const { imageDTOs } = useGalleryImages();
|
||||
const loadedImagesCount = useMemo(() => imageDTOs.length, [imageDTOs.length]);
|
||||
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectListImagesQueryArgs } from 'features/gallery/store/gallerySelectors';
|
||||
import { offsetChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { offsetChanged, selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { throttle } from 'lodash-es';
|
||||
import { useCallback, useEffect, useMemo } from 'react';
|
||||
import { useListImagesQuery } from 'services/api/endpoints/images';
|
||||
@ -56,9 +57,13 @@ const getRange = (currentPage: number, totalPages: number, siblingCount: number)
|
||||
return fullRange as (number | 'ellipsis')[];
|
||||
};
|
||||
|
||||
const selectOffset = createSelector(selectGallerySlice, (gallery) => gallery.offset);
|
||||
const selectLimit = createSelector(selectGallerySlice, (gallery) => gallery.limit);
|
||||
|
||||
export const useGalleryPagination = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { offset, limit } = useAppSelector((s) => s.gallery);
|
||||
const offset = useAppSelector(selectOffset);
|
||||
const limit = useAppSelector(selectLimit);
|
||||
const queryArgs = useAppSelector(selectListImagesQueryArgs);
|
||||
|
||||
const { count, total } = useListImagesQuery(queryArgs, {
|
||||
|
@ -2,7 +2,7 @@ import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { handlers, parseAndRecallAllMetadata, parseAndRecallPrompts } from 'features/metadata/util/handlers';
|
||||
import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal';
|
||||
import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { activeStylePresetIdChanged, selectActivePresetId } from 'features/stylePresets/store/stylePresetSlice';
|
||||
import { toast } from 'features/toast/toast';
|
||||
import { selectActiveTab } from 'features/ui/store/uiSelectors';
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
@ -13,7 +13,7 @@ import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata';
|
||||
export const useImageActions = (image_name?: string) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId);
|
||||
const activeStylePresetId = useAppSelector(selectActivePresetId);
|
||||
const activeTabName = useAppSelector(selectActiveTab);
|
||||
const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(image_name);
|
||||
const [hasMetadata, setHasMetadata] = useState(false);
|
||||
|
@ -1,6 +1,7 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { galleryImageClicked } from 'app/store/middleware/listenerMiddleware/listeners/galleryImageClicked';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectHasSelection } from 'features/gallery/store/gallerySelectors';
|
||||
import { selectGallerySlice, selectionChanged } from 'features/gallery/store/gallerySlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import type { MouseEvent } from 'react';
|
||||
@ -9,7 +10,7 @@ import type { ImageDTO } from 'services/api/types';
|
||||
|
||||
export const useMultiselect = (imageDTO?: ImageDTO) => {
|
||||
const dispatch = useAppDispatch();
|
||||
const areMultiplesSelected = useAppSelector((s) => s.gallery.selection.length > 1);
|
||||
const areMultiplesSelected = useAppSelector(selectHasSelection);
|
||||
const selectIsSelected = useMemo(
|
||||
() =>
|
||||
createSelector(selectGallerySlice, (gallery) =>
|
||||
|
@ -1,3 +1,4 @@
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import type { SkipToken } from '@reduxjs/toolkit/query';
|
||||
import { skipToken } from '@reduxjs/toolkit/query';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
@ -5,10 +6,11 @@ import { selectGallerySlice } from 'features/gallery/store/gallerySlice';
|
||||
import { ASSETS_CATEGORIES, IMAGE_CATEGORIES } from 'features/gallery/store/types';
|
||||
import type { ListBoardsArgs, ListImagesArgs } from 'services/api/types';
|
||||
|
||||
export const selectLastSelectedImage = createMemoizedSelector(
|
||||
export const selectLastSelectedImage = createSelector(
|
||||
selectGallerySlice,
|
||||
(gallery) => gallery.selection[gallery.selection.length - 1]
|
||||
);
|
||||
export const selectLastSelectedImageName = createSelector(selectLastSelectedImage, (image) => image?.image_name);
|
||||
|
||||
export const selectListImagesQueryArgs = createMemoizedSelector(
|
||||
selectGallerySlice,
|
||||
@ -33,3 +35,26 @@ export const selectListBoardsQueryArgs = createMemoizedSelector(
|
||||
include_archived: gallery.shouldShowArchivedBoards ? true : undefined,
|
||||
})
|
||||
);
|
||||
|
||||
export const selectAutoAddBoardId = createSelector(selectGallerySlice, (gallery) => gallery.autoAddBoardId);
|
||||
export const selectSelectedBoardId = createSelector(selectGallerySlice, (gallery) => gallery.selectedBoardId);
|
||||
export const selectAutoAssignBoardOnClick = createSelector(
|
||||
selectGallerySlice,
|
||||
(gallery) => gallery.autoAssignBoardOnClick
|
||||
);
|
||||
export const selectBoardSearchText = createSelector(selectGallerySlice, (gallery) => gallery.boardSearchText);
|
||||
export const selectSearchTerm = createSelector(selectGallerySlice, (gallery) => gallery.searchTerm);
|
||||
export const selectSelectionCount = createSelector(selectGallerySlice, (gallery) => gallery.selection.length);
|
||||
export const selectHasSelection = createSelector(selectSelectionCount, (count) => count > 0);
|
||||
export const selectGalleryImageMinimumWidth = createSelector(
|
||||
selectGallerySlice,
|
||||
(gallery) => gallery.galleryImageMinimumWidth
|
||||
);
|
||||
|
||||
export const selectComparisonMode = createSelector(selectGallerySlice, (gallery) => gallery.comparisonMode);
|
||||
export const selectComparisonFit = createSelector(selectGallerySlice, (gallery) => gallery.comparisonFit);
|
||||
export const selectImageToCompare = createSelector(selectGallerySlice, (gallery) => gallery.imageToCompare);
|
||||
export const selectHasImageToCompare = createSelector(selectImageToCompare, (imageToCompare) =>
|
||||
Boolean(imageToCompare)
|
||||
);
|
||||
export const selectIsImageViewerOpen = createSelector(selectGallerySlice, (gallery) => gallery.isImageViewerOpen);
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { FormControlGroup } from '@invoke-ai/ui-library';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { selectHrfEnabled } from 'features/hrf/store/hrfSlice';
|
||||
import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus';
|
||||
import { memo } from 'react';
|
||||
|
||||
@ -9,7 +10,7 @@ import ParamHrfToggle from './ParamHrfToggle';
|
||||
|
||||
export const HrfSettings = memo(() => {
|
||||
const isHRFFeatureEnabled = useFeatureStatus('hrf');
|
||||
const hrfEnabled = useAppSelector((s) => s.hrf.hrfEnabled);
|
||||
const hrfEnabled = useAppSelector(selectHrfEnabled);
|
||||
|
||||
if (!isHRFFeatureEnabled) {
|
||||
return null;
|
||||
|
@ -2,7 +2,7 @@ import type { ComboboxOnChange, ComboboxOption } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { setHrfMethod } from 'features/hrf/store/hrfSlice';
|
||||
import { selectHrfMethod, setHrfMethod } from 'features/hrf/store/hrfSlice';
|
||||
import { isParameterHRFMethod } from 'features/parameters/types/parameterSchemas';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -15,7 +15,7 @@ const options: ComboboxOption[] = [
|
||||
const ParamHrfMethodSelect = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
const hrfMethod = useAppSelector((s) => s.hrf.hrfMethod);
|
||||
const hrfMethod = useAppSelector(selectHrfMethod);
|
||||
|
||||
const onChange = useCallback<ComboboxOnChange>(
|
||||
(v) => {
|
||||
|
@ -1,19 +1,17 @@
|
||||
import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { setHrfStrength } from 'features/hrf/store/hrfSlice';
|
||||
import { selectHrfStrength, setHrfStrength } from 'features/hrf/store/hrfSlice';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
const selectHrfStrengthConfig = createSelector(selectConfigSlice, (config) => config.sd.hrfStrength);
|
||||
|
||||
const ParamHrfStrength = () => {
|
||||
const hrfStrength = useAppSelector((s) => s.hrf.hrfStrength);
|
||||
const initial = useAppSelector((s) => s.config.sd.hrfStrength.initial);
|
||||
const sliderMin = useAppSelector((s) => s.config.sd.hrfStrength.sliderMin);
|
||||
const sliderMax = useAppSelector((s) => s.config.sd.hrfStrength.sliderMax);
|
||||
const numberInputMin = useAppSelector((s) => s.config.sd.hrfStrength.numberInputMin);
|
||||
const numberInputMax = useAppSelector((s) => s.config.sd.hrfStrength.numberInputMax);
|
||||
const coarseStep = useAppSelector((s) => s.config.sd.hrfStrength.coarseStep);
|
||||
const fineStep = useAppSelector((s) => s.config.sd.hrfStrength.fineStep);
|
||||
const hrfStrength = useAppSelector(selectHrfStrength);
|
||||
const config = useAppSelector(selectHrfStrengthConfig);
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
@ -30,22 +28,22 @@ const ParamHrfStrength = () => {
|
||||
<FormLabel>{`${t('parameters.denoisingStrength')}`}</FormLabel>
|
||||
</InformationalPopover>
|
||||
<CompositeSlider
|
||||
min={sliderMin}
|
||||
max={sliderMax}
|
||||
step={coarseStep}
|
||||
fineStep={fineStep}
|
||||
min={config.sliderMin}
|
||||
max={config.sliderMax}
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
value={hrfStrength}
|
||||
defaultValue={initial}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChange}
|
||||
marks
|
||||
/>
|
||||
<CompositeNumberInput
|
||||
min={numberInputMin}
|
||||
max={numberInputMax}
|
||||
step={coarseStep}
|
||||
fineStep={fineStep}
|
||||
min={config.numberInputMin}
|
||||
max={config.numberInputMax}
|
||||
step={config.coarseStep}
|
||||
fineStep={config.fineStep}
|
||||
value={hrfStrength}
|
||||
defaultValue={initial}
|
||||
defaultValue={config.initial}
|
||||
onChange={onChange}
|
||||
/>
|
||||
</FormControl>
|
||||
|
@ -1,7 +1,7 @@
|
||||
import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { setHrfEnabled } from 'features/hrf/store/hrfSlice';
|
||||
import { selectHrfEnabled, setHrfEnabled } from 'features/hrf/store/hrfSlice';
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { memo, useCallback } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
@ -10,7 +10,7 @@ const ParamHrfToggle = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const { t } = useTranslation();
|
||||
|
||||
const hrfEnabled = useAppSelector((s) => s.hrf.hrfEnabled);
|
||||
const hrfEnabled = useAppSelector(selectHrfEnabled);
|
||||
|
||||
const handleHrfEnabled = useCallback(
|
||||
(e: ChangeEvent<HTMLInputElement>) => dispatch(setHrfEnabled(e.target.checked)),
|
||||
|
@ -1,5 +1,5 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import type { ParameterHRFMethod, ParameterStrength } from 'features/parameters/types/parameterSchemas';
|
||||
|
||||
@ -35,8 +35,6 @@ export const hrfSlice = createSlice({
|
||||
|
||||
export const { setHrfEnabled, setHrfStrength, setHrfMethod } = hrfSlice.actions;
|
||||
|
||||
export const selectHrfSlice = (state: RootState) => state.hrf;
|
||||
|
||||
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
|
||||
const migrateHRFState = (state: any): any => {
|
||||
if (!('_version' in state)) {
|
||||
@ -51,3 +49,8 @@ export const hrfPersistConfig: PersistConfig<HRFState> = {
|
||||
migrate: migrateHRFState,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
export const selectHrfSlice = (state: RootState) => state.hrf;
|
||||
export const selectHrfEnabled = createSelector(selectHrfSlice, (hrf) => hrf.hrfEnabled);
|
||||
export const selectHrfMethod = createSelector(selectHrfSlice, (hrf) => hrf.hrfMethod);
|
||||
export const selectHrfStrength = createSelector(selectHrfSlice, (hrf) => hrf.hrfStrength);
|
||||
|
@ -1,23 +1,24 @@
|
||||
import type { ChakraProps } from '@invoke-ai/ui-library';
|
||||
import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library';
|
||||
import { createMemoizedSelector } from 'app/store/createMemoizedSelector';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover';
|
||||
import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox';
|
||||
import { loraAdded, selectLoRAsSlice } from 'features/controlLayers/store/lorasSlice';
|
||||
import { selectBase } from 'features/controlLayers/store/paramsSlice';
|
||||
import { memo, useCallback, useMemo } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useLoRAModels } from 'services/api/hooks/modelsByType';
|
||||
import type { LoRAModelConfig } from 'services/api/types';
|
||||
|
||||
const selectLoRAs = createMemoizedSelector(selectLoRAsSlice, (loras) => loras.loras);
|
||||
const selectLoRAs = createSelector(selectLoRAsSlice, (loras) => loras.loras);
|
||||
|
||||
const LoRASelect = () => {
|
||||
const dispatch = useAppDispatch();
|
||||
const [modelConfigs, { isLoading }] = useLoRAModels();
|
||||
const { t } = useTranslation();
|
||||
const addedLoRAs = useAppSelector(selectLoRAs);
|
||||
const currentBaseModel = useAppSelector((s) => s.params.model?.base);
|
||||
const currentBaseModel = useAppSelector(selectBase);
|
||||
|
||||
const getIsDisabled = (model: LoRAModelConfig): boolean => {
|
||||
const isCompatible = currentBaseModel === model.base;
|
||||
|
@ -4,6 +4,7 @@ import { useAppSelector } from 'app/store/storeHooks';
|
||||
import IAIDndImage from 'common/components/IAIDndImage';
|
||||
import { IAINoContentFallback } from 'common/components/IAIImageFallback';
|
||||
import NextPrevImageButtons from 'features/gallery/components/NextPrevImageButtons';
|
||||
import { selectLastSelectedImage } from 'features/gallery/store/gallerySelectors';
|
||||
import NodeWrapper from 'features/nodes/components/flow/nodes/common/NodeWrapper';
|
||||
import { DRAG_HANDLE_CLASSNAME } from 'features/nodes/types/constants';
|
||||
import type { AnimationProps } from 'framer-motion';
|
||||
@ -15,7 +16,7 @@ import type { NodeProps } from 'reactflow';
|
||||
import { $lastProgressEvent } from 'services/events/setEventListeners';
|
||||
|
||||
const CurrentImageNode = (props: NodeProps) => {
|
||||
const imageDTO = useAppSelector((s) => s.gallery.selection[s.gallery.selection.length - 1]);
|
||||
const imageDTO = useAppSelector(selectLastSelectedImage);
|
||||
const lastProgressEvent = useStore($lastProgressEvent);
|
||||
|
||||
if (lastProgressEvent?.progress_image) {
|
||||
|
@ -1,6 +1,6 @@
|
||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
||||
import { createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig } from 'app/store/store';
|
||||
import { createSelector, createSlice } from '@reduxjs/toolkit';
|
||||
import type { PersistConfig, RootState } from 'app/store/store';
|
||||
import { stylePresetsApi } from 'services/api/endpoints/stylePresets';
|
||||
|
||||
import type { StylePresetState } from './types';
|
||||
@ -63,3 +63,9 @@ export const stylePresetPersistConfig: PersistConfig<StylePresetState> = {
|
||||
migrate: migrateStylePresetState,
|
||||
persistDenylist: [],
|
||||
};
|
||||
|
||||
export const selectStylePresetSlice = (state: RootState) => state.stylePreset;
|
||||
export const selectActivePresetId = createSelector(
|
||||
selectStylePresetSlice,
|
||||
(stylePreset) => stylePreset.activeStylePresetId
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
import type { RootState } from 'app/store/store';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { selectConfigSlice } from 'features/system/store/configSlice';
|
||||
|
||||
export const configSelector = (state: RootState) => state.config;
|
||||
export const selectAllowPrivateBoards = createSelector(selectConfigSlice, (config) => config.allowPrivateBoards);
|
||||
|
@ -1,4 +1,5 @@
|
||||
import { Flex } from '@invoke-ai/ui-library';
|
||||
import { createSelector } from '@reduxjs/toolkit';
|
||||
import { useAppSelector } from 'app/store/storeHooks';
|
||||
import { useScopeOnFocus } from 'common/hooks/interactionScopes';
|
||||
import { CanvasEditor } from 'features/controlLayers/components/ControlLayersEditor';
|
||||
@ -18,7 +19,7 @@ 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 { $isGalleryPanelOpen, $isParametersPanelOpen } from 'features/ui/store/uiSlice';
|
||||
import { $isGalleryPanelOpen, $isParametersPanelOpen, selectUiSlice } from 'features/ui/store/uiSlice';
|
||||
import type { TabName } from 'features/ui/store/uiTypes';
|
||||
import type { CSSProperties } from 'react';
|
||||
import { memo, useMemo, useRef } from 'react';
|
||||
@ -41,11 +42,18 @@ const OPTIONS_PANEL_MIN_SIZE_PCT = 20;
|
||||
const onGalleryPanelCollapse = (isCollapsed: boolean) => $isGalleryPanelOpen.set(!isCollapsed);
|
||||
const onParametersPanelCollapse = (isCollapsed: boolean) => $isParametersPanelOpen.set(!isCollapsed);
|
||||
|
||||
const selectShouldShowGalleryPanel = createSelector(selectUiSlice, (ui) =>
|
||||
TABS_WITH_GALLERY_PANEL.includes(ui.activeTab)
|
||||
);
|
||||
const selectShouldShowOptionsPanel = createSelector(selectUiSlice, (ui) =>
|
||||
TABS_WITH_OPTIONS_PANEL.includes(ui.activeTab)
|
||||
);
|
||||
|
||||
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 shouldShowGalleryPanel = useAppSelector(selectShouldShowGalleryPanel);
|
||||
const shouldShowOptionsPanel = useAppSelector(selectShouldShowOptionsPanel);
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
useScopeOnFocus('gallery', ref);
|
||||
|
||||
|
@ -1,11 +1,18 @@
|
||||
import { useAppDispatch, useAppSelector } from 'app/store/storeHooks';
|
||||
import { useAppStore } from 'app/store/nanostores/store';
|
||||
import { useAppDispatch } from 'app/store/storeHooks';
|
||||
import { panelsChanged } from 'features/ui/store/uiSlice';
|
||||
import { useCallback } from 'react';
|
||||
|
||||
export const usePanelStorage = () => {
|
||||
const store = useAppStore();
|
||||
const dispatch = useAppDispatch();
|
||||
const panels = useAppSelector((s) => s.ui.panels);
|
||||
const getItem = useCallback((name: string) => panels[name] ?? '', [panels]);
|
||||
const getItem = useCallback(
|
||||
(name: string) => {
|
||||
const panels = store.getState().ui.panels;
|
||||
return panels[name] ?? '';
|
||||
},
|
||||
[store]
|
||||
);
|
||||
const setItem = useCallback(
|
||||
(name: string, value: string) => {
|
||||
dispatch(panelsChanged({ name, value }));
|
||||
|
@ -2,3 +2,5 @@ import { createSelector } from '@reduxjs/toolkit';
|
||||
import { selectUiSlice } from 'features/ui/store/uiSlice';
|
||||
|
||||
export const selectActiveTab = createSelector(selectUiSlice, (ui) => ui.activeTab);
|
||||
export const selectShouldShowImageDetails = createSelector(selectUiSlice, (ui) => ui.shouldShowImageDetails);
|
||||
export const selectShouldShowProgressInViewer = createSelector(selectUiSlice, (ui) => ui.shouldShowProgressInViewer);
|
||||
|
Loading…
x
Reference in New Issue
Block a user