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:
psychedelicious 2024-08-27 13:19:14 +10:00
parent f126a61f66
commit a41406ca9a
92 changed files with 561 additions and 294 deletions

View File

@ -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);

View File

@ -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(() => {

View File

@ -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]);

View File

@ -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();

View File

@ -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(
() =>

View File

@ -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[]) => {

View 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();

View File

@ -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]);

View File

@ -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]);

View File

@ -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>

View File

@ -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]);

View File

@ -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) {

View File

@ -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'];

View File

@ -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]);

View File

@ -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">

View File

@ -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]

View File

@ -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]);

View File

@ -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]

View File

@ -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(
() =>

View File

@ -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;

View File

@ -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">

View File

@ -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]);

View File

@ -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)));

View File

@ -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;

View File

@ -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)));

View File

@ -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) => {

View File

@ -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(() => {

View File

@ -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) => {

View File

@ -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>
);

View File

@ -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) {

View File

@ -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));

View File

@ -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 => {

View File

@ -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);
}
);

View File

@ -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;

View File

@ -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 (

View File

@ -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(

View File

@ -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;
}

View File

@ -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>

View File

@ -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(() => {

View File

@ -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 [

View File

@ -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 (

View File

@ -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(
{},
{

View File

@ -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]
);

View File

@ -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) {

View File

@ -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(() => {

View File

@ -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 (

View File

@ -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(

View File

@ -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);

View File

@ -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' }));

View File

@ -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();

View File

@ -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 (

View File

@ -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);

View File

@ -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)),

View File

@ -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)),

View File

@ -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>) => {

View File

@ -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) => {

View File

@ -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>) => {

View File

@ -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>) => {

View File

@ -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[]>(
() => [

View File

@ -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) {

View File

@ -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);

View File

@ -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);

View File

@ -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, {

View File

@ -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);

View File

@ -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]);

View File

@ -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);

View File

@ -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) {

View File

@ -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>(

View File

@ -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

View File

@ -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>

View File

@ -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>(
() => ({

View File

@ -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();

View File

@ -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(() => {

View File

@ -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">

View File

@ -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) {

View File

@ -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]);

View File

@ -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, {

View File

@ -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);

View File

@ -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) =>

View File

@ -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);

View File

@ -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;

View File

@ -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) => {

View File

@ -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>

View File

@ -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)),

View File

@ -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);

View File

@ -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;

View File

@ -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) {

View File

@ -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
);

View File

@ -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);

View File

@ -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);

View File

@ -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 }));

View File

@ -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);