diff --git a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx index ced3037a40..ef2cb32eaa 100644 --- a/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx +++ b/invokeai/frontend/web/src/app/components/AppErrorBoundaryFallback.tsx @@ -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); diff --git a/invokeai/frontend/web/src/app/logging/useLogger.ts b/invokeai/frontend/web/src/app/logging/useLogger.ts index c86844f5ed..c0a1529747 100644 --- a/invokeai/frontend/web/src/app/logging/useLogger.ts +++ b/invokeai/frontend/web/src/app/logging/useLogger.ts @@ -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(() => { diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx index cb295bb0f4..8d67854679 100644 --- a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts index 9c84e66bfc..8b8b3fa844 100644 --- a/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts +++ b/invokeai/frontend/web/src/common/hooks/useFullscreenDropzone.ts @@ -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(false); const postUploadAction = useAppSelector(selectPostUploadAction); const [uploadImage] = useUploadImageMutation(); diff --git a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts index a06979b034..f077855ae9 100644 --- a/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts +++ b/invokeai/frontend/web/src/common/hooks/useGroupedModelCombobox.ts @@ -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 = (model: T) => model.base.toUpp const groupByBaseAndTypeFunc = (model: T) => `${model.base.toUpperCase()} / ${model.type.replaceAll('_', ' ').toUpperCase()}`; +const selectBaseWithSDXLFallback = createSelector(selectParamsSlice, (params) => params.model?.base ?? 'sdxl'); + export const useGroupedModelCombobox = ( arg: UseGroupedModelComboboxArg ): 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[]>(() => { if (!modelConfigs) { @@ -54,9 +58,9 @@ export const useGroupedModelCombobox = ( }, [] as GroupBase[] ); - _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( () => diff --git a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx index 011f49ec26..d49ce460ce 100644 --- a/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx +++ b/invokeai/frontend/web/src/common/hooks/useImageUploadButton.tsx @@ -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 = { * // 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[]) => { diff --git a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx index 489085ebf5..175bf9fede 100644 --- a/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx +++ b/invokeai/frontend/web/src/features/changeBoardModal/components/ChangeBoardModal.tsx @@ -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(); 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(); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx index 82ceb60929..993e42c57f 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuItems.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx index 7613782e6f..e052c1a214 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasModeSwitcher.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx index 406cba7d7d..16e348ef39 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/CanvasPanelContent.tsx @@ -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( () => ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx index 139f0a3af5..84f268ac23 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerControlAdapterModel.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx index 42c66ddd52..9421804090 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/ControlLayer/ControlLayerEntityList.tsx @@ -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) { diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx index fb2537e7ac..bc512dc019 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Filters/FilterTypeSelect.tsx @@ -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']; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx index b14b761951..bacd81f9e1 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/IPAdapter/IPAdapterModel.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx index ddfcfe8f3a..53dfaa718e 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsAutoSaveCheckbox.tsx @@ -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 ( diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx index 60b8cf4ff9..7795d7d1b3 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsClipToBboxCheckbox.tsx @@ -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) => dispatch(clipToBboxChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx index f29b5ac16f..a625c8e622 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsDynamicGridSwitch.tsx @@ -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]); diff --git a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx index e4a8abd1b0..a8f6d2e18a 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/Settings/CanvasSettingsInvertScrollCheckbox.tsx @@ -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) => dispatch(invertScrollChanged(e.target.checked)), [dispatch] diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx index f7679346c0..2c365ef20d 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StageComponent.tsx @@ -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( () => diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx index c5cbe43755..2f86d30f67 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaIsStagingGate.tsx @@ -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; diff --git a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx index 12d56517f5..8d875eb899 100644 --- a/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx +++ b/invokeai/frontend/web/src/features/controlLayers/components/StagingArea/StagingAreaToolbar.tsx @@ -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={} onClick={onPrev} colorScheme="invokeBlue" - isDisabled={images.length <= 1 || !shouldShowStagedImage} + isDisabled={imageCount <= 1 || !shouldShowStagedImage} />