diff --git a/invokeai/frontend/web/src/app/components/App.tsx b/invokeai/frontend/web/src/app/components/App.tsx index c9345e1039..07f484d200 100644 --- a/invokeai/frontend/web/src/app/components/App.tsx +++ b/invokeai/frontend/web/src/app/components/App.tsx @@ -18,7 +18,7 @@ import { ClearQueueConfirmationsAlertDialog } from 'features/queue/components/Cl import { StylePresetModal } from 'features/stylePresets/components/StylePresetForm/StylePresetModal'; import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; import { configChanged } from 'features/system/store/configSlice'; -import { languageSelector } from 'features/system/store/systemSelectors'; +import { selectLanguage } from 'features/system/store/systemSelectors'; import { AppContent } from 'features/ui/components/AppContent'; import { setActiveTab } from 'features/ui/store/uiSlice'; import type { TabName } from 'features/ui/store/uiTypes'; @@ -53,7 +53,7 @@ const App = ({ selectedStylePresetId, destination, }: Props) => { - const language = useAppSelector(languageSelector); + const language = useAppSelector(selectLanguage); const logger = useLogger('system'); const dispatch = useAppDispatch(); const clearStorage = useClearStorage(); diff --git a/invokeai/frontend/web/src/app/logging/useLogger.ts b/invokeai/frontend/web/src/app/logging/useLogger.ts index c0a1529747..f529716b82 100644 --- a/invokeai/frontend/web/src/app/logging/useLogger.ts +++ b/invokeai/frontend/web/src/app/logging/useLogger.ts @@ -1,21 +1,20 @@ -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 { + selectSystemLogIsEnabled, + selectSystemLogLevel, + selectSystemLogNamespaces, +} 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(selectLogLevel); - const logNamespaces = useAppSelector(selectLogNamespaces); - const logIsEnabled = useAppSelector(selectLogIsEnabled); + const logLevel = useAppSelector(selectSystemLogLevel); + const logNamespaces = useAppSelector(selectSystemLogNamespaces); + const logIsEnabled = useAppSelector(selectSystemLogIsEnabled); // The provided Roarr browser log writer uses localStorage to config logging to console useEffect(() => { diff --git a/invokeai/frontend/web/src/app/store/constants.ts b/invokeai/frontend/web/src/app/store/constants.ts index 14a2c0b77f..58989b4a54 100644 --- a/invokeai/frontend/web/src/app/store/constants.ts +++ b/invokeai/frontend/web/src/app/store/constants.ts @@ -1,2 +1,3 @@ export const STORAGE_PREFIX = '@@invokeai-'; export const EMPTY_ARRAY = []; +export const EMPTY_OBJECT = {}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts index 0e30802328..5b52765c9c 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/boardAndImagesDeleted.ts @@ -1,7 +1,8 @@ import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; -import { nodeEditorReset, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { imagesApi } from 'services/api/endpoints/images'; export const addDeleteBoardAndImagesFulfilledListener = (startAppListening: AppStartListening) => { diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts index 88cb2b5f7b..847c64f3c3 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/enqueueRequestedNodes.ts @@ -1,5 +1,6 @@ import { enqueueRequested } from 'app/store/actions'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { buildNodesGraph } from 'features/nodes/util/graph/buildNodesGraph'; import { buildWorkflowWithValidation } from 'features/nodes/util/workflow/buildWorkflow'; import { queueApi } from 'services/api/endpoints/queue'; @@ -11,12 +12,12 @@ export const addEnqueueRequestedNodes = (startAppListening: AppStartListening) = enqueueRequested.match(action) && action.payload.tabName === 'workflows', effect: async (action, { getState, dispatch }) => { const state = getState(); - const { nodes, edges } = state.nodes.present; + const nodes = selectNodesSlice(state); const workflow = state.workflow; - const graph = buildNodesGraph(state.nodes.present); + const graph = buildNodesGraph(nodes); const builtWorkflow = buildWorkflowWithValidation({ - nodes, - edges, + nodes: nodes.nodes, + edges: nodes.edges, workflow, }); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts index 834b961ece..690bb77fa1 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/updateAllNodesRequested.ts @@ -2,6 +2,7 @@ import { logger } from 'app/logging/logger'; import type { AppStartListening } from 'app/store/middleware/listenerMiddleware'; import { updateAllNodesRequested } from 'features/nodes/store/actions'; import { $templates, nodesChanged } from 'features/nodes/store/nodesSlice'; +import { selectNodes } from 'features/nodes/store/selectors'; import { NodeUpdateError } from 'features/nodes/types/error'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate, updateNode } from 'features/nodes/util/node/nodeUpdate'; @@ -14,7 +15,7 @@ export const addUpdateAllNodesRequestedListener = (startAppListening: AppStartLi startAppListening({ actionCreator: updateAllNodesRequested, effect: (action, { dispatch, getState }) => { - const { nodes } = getState().nodes.present; + const nodes = selectNodes(getState()); const templates = $templates.get(); let unableToUpdateCount = 0; diff --git a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx index 8d67854679..04126647aa 100644 --- a/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx +++ b/invokeai/frontend/web/src/common/components/InformationalPopover/InformationalPopover.tsx @@ -32,7 +32,10 @@ type Props = { children: ReactElement; }; -const selectShouldEnableInformationalPopovers = createSelector(selectSystemSlice, system => system.shouldEnableInformationalPopovers); +const selectShouldEnableInformationalPopovers = createSelector( + selectSystemSlice, + (system) => system.shouldEnableInformationalPopovers +); export const InformationalPopover = memo(({ feature, children, inPortal = true, ...rest }: Props) => { const shouldEnableInformationalPopovers = useAppSelector(selectShouldEnableInformationalPopovers); diff --git a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts index c3946a2f0a..84dbfd3dc2 100644 --- a/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts +++ b/invokeai/frontend/web/src/common/hooks/useIsReadyToEnqueue.ts @@ -6,7 +6,8 @@ import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; diff --git a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts index 66d5521113..71f88b8934 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/lorasSlice.ts @@ -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 { LoRA } from 'features/controlLayers/store/types'; import { zModelIdentifierField } from 'features/nodes/types/common'; @@ -65,8 +65,6 @@ export const lorasSlice = createSlice({ export const { loraAdded, loraRecalled, loraDeleted, loraWeightChanged, loraIsEnabledChanged, loraAllDeleted } = lorasSlice.actions; -export const selectLoRAsSlice = (state: RootState) => state.loras; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrate = (state: any): any => { return state; @@ -78,3 +76,6 @@ export const lorasPersistConfig: PersistConfig = { migrate, persistDenylist: [], }; + +export const selectLoRAsSlice = (state: RootState) => state.loras; +export const selectAddedLoRAs = createSelector(selectLoRAsSlice, (loras) => loras.loras); diff --git a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts index a1f3916215..7a34309388 100644 --- a/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts +++ b/invokeai/frontend/web/src/features/controlLayers/store/paramsSlice.ts @@ -1,4 +1,5 @@ -import { createSelector, createSlice, type PayloadAction } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSelector, createSlice } 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'; @@ -270,9 +271,6 @@ export const { modelChanged, } = 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 => { return state; @@ -284,3 +282,55 @@ export const paramsPersistConfig: PersistConfig = { migrate, persistDenylist: [], }; + +export const selectParamsSlice = (state: RootState) => state.params; +export const createParamsSelector = (selector: Selector) => + createSelector(selectParamsSlice, selector); + +export const selectBase = createParamsSelector((params) => params.model?.base); +export const selectIsSDXL = createParamsSelector((params) => params.model?.base === 'sdxl'); +export const selectModel = createParamsSelector((params) => params.model); +export const selectModelKey = createParamsSelector((params) => params.model?.key); +export const selectVAE = createParamsSelector((params) => params.vae); +export const selectVAEKey = createParamsSelector((params) => params.vae?.key); +export const selectCFGScale = createParamsSelector((params) => params.cfgScale); +export const selectSteps = createParamsSelector((params) => params.steps); +export const selectCFGRescaleMultiplier = createParamsSelector((params) => params.cfgRescaleMultiplier); +export const selectCLIPSKip = createParamsSelector((params) => params.clipSkip); +export const selectCanvasCoherenceEdgeSize = createParamsSelector((params) => params.canvasCoherenceEdgeSize); +export const selectCanvasCoherenceMinDenoise = createParamsSelector((params) => params.canvasCoherenceMinDenoise); +export const selectCanvasCoherenceMode = createParamsSelector((params) => params.canvasCoherenceMode); +export const selectMaskBlur = createParamsSelector((params) => params.maskBlur); +export const selectInfillMethod = createParamsSelector((params) => params.infillMethod); +export const selectInfillTileSize = createParamsSelector((params) => params.infillTileSize); +export const selectInfillPatchmatchDownscaleSize = createParamsSelector( + (params) => params.infillPatchmatchDownscaleSize +); +export const selectInfillColorValue = createParamsSelector((params) => params.infillColorValue); +export const selectImg2imgStrength = createParamsSelector((params) => params.img2imgStrength); +export const selectPositivePrompt = createParamsSelector((params) => params.positivePrompt); +export const selectNegativePrompt = createParamsSelector((params) => params.negativePrompt); +export const selectPositivePrompt2 = createParamsSelector((params) => params.positivePrompt2); +export const selectNegativePrompt2 = createParamsSelector((params) => params.negativePrompt2); +export const selectShouldConcatPrompts = createParamsSelector((params) => params.shouldConcatPrompts); +export const selectScheduler = createParamsSelector((params) => params.scheduler); +export const selectSeamlessXAxis = createParamsSelector((params) => params.seamlessXAxis); +export const selectSeamlessYAxis = createParamsSelector((params) => params.seamlessYAxis); +export const selectSeed = createParamsSelector((params) => params.seed); +export const selectShouldRandomizeSeed = createParamsSelector((params) => params.shouldConcatPrompts); +export const selectVAEPrecision = createParamsSelector((params) => params.vaePrecision); +export const selectIterations = createParamsSelector((params) => params.iterations); +export const selectShouldUseCPUNoise = createParamsSelector((params) => params.shouldUseCpuNoise); + +export const selectRefinerCFGScale = createParamsSelector((params) => params.refinerCFGScale); +export const selectRefinerModel = createParamsSelector((params) => params.refinerModel); +export const selectIsRefinerModelSelected = createParamsSelector((params) => Boolean(params.refinerModel)); +export const selectRefinerPositiveAestheticScore = createParamsSelector( + (params) => params.refinerPositiveAestheticScore +); +export const selectRefinerNegativeAestheticScore = createParamsSelector( + (params) => params.refinerNegativeAestheticScore +); +export const selectRefinerScheduler = createParamsSelector((params) => params.refinerScheduler); +export const selectRefinerStart = createParamsSelector((params) => params.refinerStart); +export const selectRefinerSteps = createParamsSelector((params) => params.refinerSteps); diff --git a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx index 45b1a3bc84..0cd4630243 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx +++ b/invokeai/frontend/web/src/features/deleteImageModal/components/DeleteImageModal.tsx @@ -11,7 +11,7 @@ import { selectDeleteImageModalSlice, } from 'features/deleteImageModal/store/slice'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { selectSystemSlice, setShouldConfirmOnDelete } from 'features/system/store/systemSlice'; import { some } from 'lodash-es'; import type { ChangeEvent } from 'react'; diff --git a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts index 7aa602b522..ea872d2272 100644 --- a/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts +++ b/invokeai/frontend/web/src/features/deleteImageModal/store/selectors.ts @@ -2,7 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import type { CanvasState } from 'features/controlLayers/store/types'; import { selectDeleteImageModalSlice } from 'features/deleteImageModal/store/slice'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { NodesState } from 'features/nodes/store/types'; import { isImageFieldInputInstance } from 'features/nodes/types/field'; import { isInvocationNode } from 'features/nodes/types/invocation'; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx index 6c614d0d54..cc363c8122 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsMaxPrompts.tsx @@ -1,20 +1,19 @@ 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, selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; -import { selectConfigSlice } from 'features/system/store/configSlice'; +import { + maxPromptsChanged, + selectDynamicPromptsCombinatorial, + selectDynamicPromptsMaxPrompts, +} from 'features/dynamicPrompts/store/dynamicPromptsSlice'; +import { selectMaxPromptsConfig } 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(selectMaxPrompts); + const maxPrompts = useAppSelector(selectDynamicPromptsMaxPrompts); const config = useAppSelector(selectMaxPromptsConfig); - const isDisabled = useAppSelector(selectIsDisabled); + const combinatorial = useAppSelector(selectDynamicPromptsCombinatorial); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -26,7 +25,7 @@ const ParamDynamicPromptsMaxPrompts = () => { ); return ( - + {t('dynamicPrompts.maxPrompts')} diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx index 8044c97297..b5de314e65 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsPreview.tsx @@ -1,11 +1,15 @@ import type { ChakraProps } from '@invoke-ai/ui-library'; import { Flex, FormControl, FormLabel, ListItem, OrderedList, Spinner, Text } from '@invoke-ai/ui-library'; -import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; +import { + selectDynamicPromptsIsError, + selectDynamicPromptsIsLoading, + selectDynamicPromptsParsingError, + selectDynamicPromptsPrompts, +} from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiWarningCircleBold } from 'react-icons/pi'; @@ -14,17 +18,12 @@ 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(selectParsingError); - const isError = useAppSelector(selectIsError); - const isLoading = useAppSelector(selectIsLoading); - const prompts = useAppSelector(selectPrompts); + const parsingError = useAppSelector(selectDynamicPromptsParsingError); + const isError = useAppSelector(selectDynamicPromptsIsError); + const isLoading = useAppSelector(selectDynamicPromptsIsLoading); + const prompts = useAppSelector(selectDynamicPromptsPrompts); const label = useMemo(() => { let _label = `${t('dynamicPrompts.promptsPreview')} (${prompts.length})`; diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx index fa86190fe7..252f3aa05f 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ParamDynamicPromptsSeedBehaviour.tsx @@ -1,22 +1,19 @@ 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, - selectDynamicPromptsSlice, + selectDynamicPromptsSeedBehaviour, } 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(selectSeedBehaviour); + const seedBehaviour = useAppSelector(selectDynamicPromptsSeedBehaviour); const options = useMemo(() => { return [ diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx b/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx index a70f8f56ad..3710443098 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx +++ b/invokeai/frontend/web/src/features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton.tsx @@ -1,9 +1,11 @@ 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 { + selectDynamicPromptsIsError, + selectDynamicPromptsIsLoading, +} from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { BsBracesAsterisk } from 'react-icons/bs'; @@ -12,15 +14,10 @@ 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(selectIsLoading); - const isError = useAppSelector(selectIsError); + const isLoading = useAppSelector(selectDynamicPromptsIsLoading); + const isError = useAppSelector(selectDynamicPromptsIsError); const { isOpen, onOpen } = useDynamicPromptsModal(); return ( diff --git a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts index d73db3e810..6e700d5629 100644 --- a/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts +++ b/invokeai/frontend/web/src/features/dynamicPrompts/store/dynamicPromptsSlice.ts @@ -1,5 +1,5 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { z } from 'zod'; @@ -72,8 +72,6 @@ export const { seedBehaviourChanged, } = dynamicPromptsSlice.actions; -export const selectDynamicPromptsSlice = (state: RootState) => state.dynamicPrompts; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateDynamicPromptsState = (state: any): any => { if (!('_version' in state)) { @@ -88,3 +86,23 @@ export const dynamicPromptsPersistConfig: PersistConfig = { migrate: migrateDynamicPromptsState, persistDenylist: ['prompts'], }; + +export const selectDynamicPromptsSlice = (state: RootState) => state.dynamicPrompts; +const createDynamicPromptsSelector = (selector: Selector) => + createSelector(selectDynamicPromptsSlice, selector); + +export const selectDynamicPromptsMaxPrompts = createDynamicPromptsSelector( + (dynamicPrompts) => dynamicPrompts.maxPrompts +); +export const selectDynamicPromptsCombinatorial = createDynamicPromptsSelector( + (dynamicPrompts) => dynamicPrompts.combinatorial +); +export const selectDynamicPromptsPrompts = createDynamicPromptsSelector((dynamicPrompts) => dynamicPrompts.prompts); +export const selectDynamicPromptsParsingError = createDynamicPromptsSelector( + (dynamicPrompts) => dynamicPrompts.parsingError +); +export const selectDynamicPromptsIsError = createDynamicPromptsSelector((dynamicPrompts) => dynamicPrompts.isError); +export const selectDynamicPromptsIsLoading = createDynamicPromptsSelector((dynamicPrompts) => dynamicPrompts.isLoading); +export const selectDynamicPromptsSeedBehaviour = createDynamicPromptsSelector( + (dynamicPrompts) => dynamicPrompts.seedBehaviour +); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx index 4599216ea2..9b4689934e 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/BoardsList/BoardsList.tsx @@ -22,7 +22,6 @@ type Props = { setBoardToDelete: (board?: BoardDTO) => void; }; - export const BoardsList = ({ isPrivate, setBoardToDelete }: Props) => { const { t } = useTranslation(); const selectedBoardId = useAppSelector(selectSelectedBoardId); diff --git a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx index 458f2fd78c..de0126dd1f 100644 --- a/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx +++ b/invokeai/frontend/web/src/features/gallery/components/Boards/DeleteBoardModal.tsx @@ -17,7 +17,7 @@ import { selectCanvasSlice } from 'features/controlLayers/store/selectors'; import ImageUsageMessage from 'features/deleteImageModal/components/ImageUsageMessage'; import { getImageUsage } from 'features/deleteImageModal/store/selectors'; import type { ImageUsage } from 'features/deleteImageModal/store/types'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { some } from 'lodash-es'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts index bc5eb6020c..0d656d3b3c 100644 --- a/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts +++ b/invokeai/frontend/web/src/features/gallery/hooks/useImageActions.ts @@ -2,7 +2,10 @@ 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, selectActivePresetId } from 'features/stylePresets/store/stylePresetSlice'; +import { + activeStylePresetIdChanged, + selectStylePresetActivePresetId, +} 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 +16,7 @@ import { useDebouncedMetadata } from 'services/api/hooks/useDebouncedMetadata'; export const useImageActions = (image_name?: string) => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const activeStylePresetId = useAppSelector(selectActivePresetId); + const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); const activeTabName = useAppSelector(selectActiveTab); const { metadata, isLoading: isLoadingMetadata } = useDebouncedMetadata(image_name); const [hasMetadata, setHasMetadata] = useState(false); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts index 8a7e3a7aa8..799eb1cbd1 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts +++ b/invokeai/frontend/web/src/features/modelManagerV2/store/modelManagerV2Slice.ts @@ -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 { ModelType } from 'services/api/types'; @@ -50,8 +50,6 @@ export const modelManagerV2Slice = createSlice({ export const { setSelectedModelKey, setSearchTerm, setFilteredModelType, setSelectedModelMode, setScanPath } = modelManagerV2Slice.actions; -export const selectModelManagerV2Slice = (state: RootState) => state.modelmanagerV2; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateModelManagerState = (state: any): any => { if (!('_version' in state)) { @@ -66,3 +64,13 @@ export const modelManagerV2PersistConfig: PersistConfig = { migrate: migrateModelManagerState, persistDenylist: ['selectedModelKey', 'selectedModelMode', 'filteredModelType', 'searchTerm'], }; + +export const selectModelManagerV2Slice = (state: RootState) => state.modelmanagerV2; + +export const createModelManagerSelector = (selector: (state: ModelManagerState) => T) => + createSelector(selectModelManagerV2Slice, selector); + +export const selectSelectedModelKey = createModelManagerSelector((modelManager) => modelManager.selectedModelKey); +export const selectSelectedModelMode = createModelManagerSelector((modelManager) => modelManager.selectedModelMode); +export const selectSearchTerm = createModelManagerSelector((mm) => mm.searchTerm); +export const selectFilteredModelType = createModelManagerSelector((mm) => mm.filteredModelType); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderForm.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderForm.tsx index a78405eb32..d95196b46b 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderForm.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/AddModelPanel/ScanFolder/ScanFolderForm.tsx @@ -1,6 +1,6 @@ import { Button, Flex, FormControl, FormErrorMessage, FormHelperText, FormLabel, Input } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setScanPath } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { createModelManagerSelector, setScanPath } from 'features/modelManagerV2/store/modelManagerV2Slice'; import type { ChangeEventHandler } from 'react'; import { memo, useCallback, useState } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,8 +8,10 @@ import { useLazyScanFolderQuery } from 'services/api/endpoints/models'; import { ScanModelsResults } from './ScanFolderResults'; +const selectScanPath = createModelManagerSelector((mm) => mm.scanPath); + export const ScanModelsForm = memo(() => { - const scanPath = useAppSelector((state) => state.modelmanagerV2.scanPath); + const scanPath = useAppSelector(selectScanPath); const dispatch = useAppDispatch(); const [errorMessage, setErrorMessage] = useState(''); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx index 2d781648a5..f474e55973 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelList.tsx @@ -1,7 +1,11 @@ import { Flex, Text } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; -import type { FilterableModelType } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { + type FilterableModelType, + selectFilteredModelType, + selectSearchTerm, +} from 'features/modelManagerV2/store/modelManagerV2Slice'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -23,8 +27,8 @@ import { FetchingModelsLoader } from './FetchingModelsLoader'; import { ModelListWrapper } from './ModelListWrapper'; const ModelList = () => { - const filteredModelType = useAppSelector((s) => s.modelmanagerV2.filteredModelType); - const searchTerm = useAppSelector((s) => s.modelmanagerV2.searchTerm); + const filteredModelType = useAppSelector(selectFilteredModelType); + const searchTerm = useAppSelector(selectSearchTerm); const { t } = useTranslation(); const [mainModels, { isLoading: isLoadingMainModels }] = useMainModels(); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx index 766b4f27b8..c0a85216a9 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelListNavigation.tsx @@ -1,6 +1,6 @@ import { Flex, IconButton, Input, InputGroup, InputRightElement, Spacer } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setSearchTerm } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { selectSearchTerm, setSearchTerm } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { t } from 'i18next'; import type { ChangeEventHandler } from 'react'; import { memo, useCallback } from 'react'; @@ -10,7 +10,7 @@ import { ModelTypeFilter } from './ModelTypeFilter'; export const ModelListNavigation = memo(() => { const dispatch = useAppDispatch(); - const searchTerm = useAppSelector((s) => s.modelmanagerV2.searchTerm); + const searchTerm = useAppSelector(selectSearchTerm); const handleSearch: ChangeEventHandler = useCallback( (event) => { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx index 402a00908b..f5b9ed8fef 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelManagerPanel/ModelTypeFilter.tsx @@ -1,7 +1,7 @@ import { Button, Menu, MenuButton, MenuItem, MenuList } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import type { FilterableModelType } from 'features/modelManagerV2/store/modelManagerV2Slice'; -import { setFilteredModelType } from 'features/modelManagerV2/store/modelManagerV2Slice'; +import { selectFilteredModelType, setFilteredModelType } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiFunnelBold } from 'react-icons/pi'; @@ -26,7 +26,7 @@ export const ModelTypeFilter = memo(() => { }), [t] ); - const filteredModelType = useAppSelector((s) => s.modelmanagerV2.filteredModelType); + const filteredModelType = useAppSelector(selectFilteredModelType); const selectModelType = useCallback( (option: FilterableModelType) => { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx index eb85434d36..415257a058 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPane.tsx @@ -1,12 +1,13 @@ import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { memo } from 'react'; import { InstallModels } from './InstallModels'; import { Model } from './ModelPanel/Model'; export const ModelPane = memo(() => { - const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const selectedModelKey = useAppSelector(selectSelectedModelKey); return ( {selectedModelKey ? : } diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgRescaleMultiplier.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgRescaleMultiplier.tsx index cde1b8d7f3..2fa9580e42 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgRescaleMultiplier.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgRescaleMultiplier.tsx @@ -2,6 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle'; +import { selectCFGRescaleMultiplierConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import type { UseControllerProps } from 'react-hook-form'; import { useController } from 'react-hook-form'; @@ -14,14 +15,12 @@ type DefaultCfgRescaleMultiplierType = MainModelDefaultSettingsFormData['cfgResc export const DefaultCfgRescaleMultiplier = memo((props: UseControllerProps) => { const { field } = useController(props); - const sliderMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.fineStep); + const config = useAppSelector(selectCFGRescaleMultiplierConfig); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + const marks = useMemo( + () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], + [config.sliderMax, config.sliderMin] + ); const onChange = useCallback( (v: number) => { @@ -54,20 +53,20 @@ export const DefaultCfgRescaleMultiplier = memo((props: UseControllerProps diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgScale.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgScale.tsx index 85d11daba8..78558243ae 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgScale.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultCfgScale.tsx @@ -2,6 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle'; +import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import type { UseControllerProps } from 'react-hook-form'; import { useController } from 'react-hook-form'; @@ -14,14 +15,12 @@ type DefaultCfgType = MainModelDefaultSettingsFormData['cfgScale']; export const DefaultCfgScale = memo((props: UseControllerProps) => { const { field } = useController(props); - const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.guidance.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.guidance.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.guidance.fineStep); + const config = useAppSelector(selectCFGScaleConfig); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + const marks = useMemo( + () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], + [config.sliderMax, config.sliderMin] + ); const onChange = useCallback( (v: number) => { @@ -54,20 +53,20 @@ export const DefaultCfgScale = memo((props: UseControllerProps diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight.tsx index 8dc00e6239..78706b8223 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight.tsx @@ -2,6 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle'; +import { selectHeightConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import type { UseControllerProps } from 'react-hook-form'; import { useController } from 'react-hook-form'; @@ -18,14 +19,12 @@ type Props = { export const DefaultHeight = memo(({ control, optimalDimension }: Props) => { const { field } = useController({ control, name: 'height' }); - const sliderMin = useAppSelector((s) => s.config.sd.height.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.height.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.height.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.height.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.height.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.height.fineStep); + const config = useAppSelector(selectHeightConfig); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); + const marks = useMemo( + () => [config.sliderMin, optimalDimension, config.sliderMax], + [config.sliderMin, optimalDimension, config.sliderMax] + ); const onChange = useCallback( (v: number) => { @@ -58,20 +57,20 @@ export const DefaultHeight = memo(({ control, optimalDimension }: Props) => { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultSteps.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultSteps.tsx index 5bfb268d08..42afa6a107 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultSteps.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultSteps.tsx @@ -2,6 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle'; +import { selectStepsConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import type { UseControllerProps } from 'react-hook-form'; import { useController } from 'react-hook-form'; @@ -14,14 +15,12 @@ type DefaultSteps = MainModelDefaultSettingsFormData['steps']; export const DefaultSteps = memo((props: UseControllerProps) => { const { field } = useController(props); - const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.steps.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.steps.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.steps.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.steps.fineStep); + const config = useAppSelector(selectStepsConfig); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + const marks = useMemo( + () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], + [config.sliderMax, config.sliderMin] + ); const onChange = useCallback( (v: number) => { @@ -54,20 +53,20 @@ export const DefaultSteps = memo((props: UseControllerProps diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultVae.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultVae.tsx index cf1578d9f4..e8667092a0 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultVae.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultVae.tsx @@ -3,6 +3,7 @@ import { Combobox, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { selectSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle'; import { memo, useCallback, useMemo } from 'react'; import type { UseControllerProps } from 'react-hook-form'; @@ -18,7 +19,7 @@ type DefaultVaeType = MainModelDefaultSettingsFormData['vae']; export const DefaultVae = memo((props: UseControllerProps) => { const { t } = useTranslation(); const { field } = useController(props); - const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const selectedModelKey = useAppSelector(selectSelectedModelKey); const { data: modelData } = useGetModelConfigQuery(selectedModelKey ?? skipToken); const [vaeModels] = useVAEModels(); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth.tsx index 8986b9820b..66467617d0 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth.tsx @@ -2,6 +2,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } f import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { SettingToggle } from 'features/modelManagerV2/subpanels/ModelPanel/SettingToggle'; +import { selectWidthConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import type { UseControllerProps } from 'react-hook-form'; import { useController } from 'react-hook-form'; @@ -18,14 +19,12 @@ type Props = { export const DefaultWidth = memo(({ control, optimalDimension }: Props) => { const { field } = useController({ control, name: 'width' }); - const sliderMin = useAppSelector((s) => s.config.sd.width.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.width.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.width.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.width.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.width.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.width.fineStep); + const config = useAppSelector(selectWidthConfig); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, optimalDimension, sliderMax], [sliderMin, optimalDimension, sliderMax]); + const marks = useMemo( + () => [config.sliderMin, optimalDimension, config.sliderMax], + [config.sliderMin, optimalDimension, config.sliderMax] + ); const onChange = useCallback( (v: number) => { @@ -58,20 +57,20 @@ export const DefaultWidth = memo(({ control, optimalDimension }: Props) => { diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx index 095b452dfc..9de6233730 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/MainModelDefaultSettings.tsx @@ -1,6 +1,7 @@ import { Button, Flex, Heading, SimpleGrid } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { useMainModelDefaultSettings } from 'features/modelManagerV2/hooks/useMainModelDefaultSettings'; +import { selectSelectedModelKey } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { DefaultHeight } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultHeight'; import { DefaultWidth } from 'features/modelManagerV2/subpanels/ModelPanel/MainModelDefaultSettings/DefaultWidth'; import type { ParameterScheduler } from 'features/parameters/types/parameterSchemas'; @@ -42,7 +43,7 @@ type Props = { }; export const MainModelDefaultSettings = memo(({ modelConfig }: Props) => { - const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const selectedModelKey = useAppSelector(selectSelectedModelKey); const { t } = useTranslation(); const defaultSettingsDefaults = useMainModelDefaultSettings(modelConfig); diff --git a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx index 2934ac916a..0393e32233 100644 --- a/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx +++ b/invokeai/frontend/web/src/features/modelManagerV2/subpanels/ModelPanel/Model.tsx @@ -1,5 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback, IAINoContentFallbackWithSpinner } from 'common/components/IAIImageFallback'; +import { selectSelectedModelKey, selectSelectedModelMode } from 'features/modelManagerV2/store/modelManagerV2Slice'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiExclamationMarkBold } from 'react-icons/pi'; @@ -10,8 +11,8 @@ import { ModelView } from './ModelView'; export const Model = memo(() => { const { t } = useTranslation(); - const selectedModelMode = useAppSelector((s) => s.modelmanagerV2.selectedModelMode); - const selectedModelKey = useAppSelector((s) => s.modelmanagerV2.selectedModelKey); + const selectedModelMode = useAppSelector(selectSelectedModelMode); + const selectedModelKey = useAppSelector(selectSelectedModelKey); const { data: modelConfigs, isLoading } = useGetModelConfigsQuery(); const modelConfig = useMemo(() => { if (!modelConfigs) { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx index 923c44b527..cb6516efd9 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/AddNodePopover/AddNodePopover.tsx @@ -18,6 +18,7 @@ import { nodesChanged, openAddNodePopover, } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; @@ -137,7 +138,7 @@ const AddNodePopover = () => { // Find a cozy spot for the node const cursorPos = $cursorPos.get(); - const { nodes, edges } = store.getState().nodes.present; + const { nodes, edges } = selectNodesSlice(store.getState()); node.position = findUnoccupiedPosition(nodes, cursorPos?.x ?? node.position.x, cursorPos?.y ?? node.position.y); node.selected = true; @@ -184,7 +185,7 @@ const AddNodePopover = () => { const target = handleType === 'target' ? pendingConnection.nodeId : node.id; const targetHandle = handleType === 'target' ? pendingConnection.handleId : null; - const { nodes, edges } = store.getState().nodes.present; + const { nodes, edges } = selectNodesSlice(store.getState()); const connection = getFirstValidConnection( source, sourceHandle, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx index a6a5cb7981..f85131a133 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/Flow.tsx @@ -21,7 +21,15 @@ import { undo, } from 'features/nodes/store/nodesSlice'; import { $flow, $needsFit } from 'features/nodes/store/reactFlowInstance'; +import { + selectEdges, + selectMayRedo, + selectMayUndo, + selectNodes, + selectNodesSlice, +} from 'features/nodes/store/selectors'; import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; +import { selectSelectionMode, selectShouldSnapToGrid } from 'features/nodes/store/workflowSettingsSlice'; import type { CSSProperties, MouseEvent } from 'react'; import { memo, useCallback, useMemo, useRef } from 'react'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -66,14 +74,14 @@ const selectCancelConnection = (state: ReactFlowState) => state.cancelConnection export const Flow = memo(() => { const dispatch = useAppDispatch(); - const nodes = useAppSelector((s) => s.nodes.present.nodes); - const edges = useAppSelector((s) => s.nodes.present.edges); + const nodes = useAppSelector(selectNodes); + const edges = useAppSelector(selectEdges); const viewport = useStore($viewport); const needsFit = useStore($needsFit); - const mayUndo = useAppSelector((s) => s.nodes.past.length > 0); - const mayRedo = useAppSelector((s) => s.nodes.future.length > 0); - const shouldSnapToGrid = useAppSelector((s) => s.workflowSettings.shouldSnapToGrid); - const selectionMode = useAppSelector((s) => s.workflowSettings.selectionMode); + const mayUndo = useAppSelector(selectMayUndo); + const mayRedo = useAppSelector(selectMayRedo); + const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid); + const selectionMode = useAppSelector(selectSelectionMode); const { onConnectStart, onConnect, onConnectEnd } = useConnection(); const flowWrapper = useRef(null); const isValidConnection = useIsValidConnection(); @@ -214,7 +222,7 @@ export const Flow = memo(() => { const onSelectAllHotkey = useCallback( (e: KeyboardEvent) => { e.preventDefault(); - const { nodes, edges } = store.getState().nodes.present; + const { nodes, edges } = selectNodesSlice(store.getState()); const nodeChanges: NodeChange[] = []; const edgeChanges: EdgeChange[] = []; nodes.forEach(({ id, selected }) => { @@ -280,7 +288,7 @@ export const Flow = memo(() => { useHotkeys('esc', onEscapeHotkey); const onDeleteHotkey = useCallback(() => { - const { nodes, edges } = store.getState().nodes.present; + const { nodes, edges } = selectNodesSlice(store.getState()); const nodeChanges: NodeChange[] = []; const edgeChanges: EdgeChange[] = []; nodes diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx index 09c88c713b..b55fc6fe70 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/connectionLines/CustomConnectionLine.tsx @@ -3,6 +3,7 @@ import { useAppSelector } from 'app/store/storeHooks'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { getFieldColor } from 'features/nodes/components/flow/edges/util/getEdgeColor'; import { $pendingConnection } from 'features/nodes/store/nodesSlice'; +import { selectShouldAnimateEdges, selectShouldColorEdges } from 'features/nodes/store/workflowSettingsSlice'; import type { CSSProperties } from 'react'; import { memo, useMemo } from 'react'; import type { ConnectionLineComponentProps } from 'reactflow'; @@ -12,8 +13,8 @@ const pathStyles: CSSProperties = { opacity: 0.8 }; const CustomConnectionLine = ({ fromX, fromY, fromPosition, toX, toY, toPosition }: ConnectionLineComponentProps) => { const pendingConnection = useStore($pendingConnection); - const shouldColorEdges = useAppSelector((state) => state.workflowSettings.shouldColorEdges); - const shouldAnimateEdges = useAppSelector((state) => state.workflowSettings.shouldAnimateEdges); + const shouldColorEdges = useAppSelector(selectShouldColorEdges); + const shouldAnimateEdges = useAppSelector(selectShouldAnimateEdges); const stroke = useMemo(() => { if (shouldColorEdges && pendingConnection) { return getFieldColor(pendingConnection.fieldTemplate.type); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx index 5a27e974e5..f9e4deed9e 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/InvocationDefaultEdge.tsx @@ -3,6 +3,7 @@ import { useStore } from '@nanostores/react'; import { useAppSelector } from 'app/store/storeHooks'; import { getEdgeStyles } from 'features/nodes/components/flow/edges/util/getEdgeColor'; import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectShouldShowEdgeLabels } from 'features/nodes/store/workflowSettingsSlice'; import { memo, useMemo } from 'react'; import type { EdgeProps } from 'reactflow'; import { BaseEdge, EdgeLabelRenderer, getBezierPath } from 'reactflow'; @@ -30,7 +31,7 @@ const InvocationDefaultEdge = ({ ); const { shouldAnimateEdges, areConnectedNodesSelected, stroke, label } = useAppSelector(selector); - const shouldShowEdgeLabels = useAppSelector((s) => s.workflowSettings.shouldShowEdgeLabels); + const shouldShowEdgeLabels = useAppSelector(selectShouldShowEdgeLabels); const [edgePath, labelX, labelY] = getBezierPath({ sourceX, diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts index 9c67728722..6a783f3158 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/components/flow/edges/util/makeEdgeSelector.ts @@ -1,7 +1,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { colorTokenToCssVar } from 'common/util/colorTokenToCssVar'; import { deepClone } from 'common/util/deepClone'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { Templates } from 'features/nodes/store/types'; import { selectWorkflowSettingsSlice } from 'features/nodes/store/workflowSettingsSlice'; import { isInvocationNode } from 'features/nodes/types/invocation'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx index 1d12b6a454..ade19d6f89 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/InvocationNodeWrapper.tsx @@ -1,7 +1,9 @@ import { useStore } from '@nanostores/react'; +import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import InvocationNode from 'features/nodes/components/flow/nodes/Invocation/InvocationNode'; import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectNodes } from 'features/nodes/store/selectors'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import type { NodeProps } from 'reactflow'; @@ -13,7 +15,11 @@ const InvocationNodeWrapper = (props: NodeProps) => { const { id: nodeId, type, isOpen, label } = data; const templates = useStore($templates); const hasTemplate = useMemo(() => Boolean(templates[type]), [templates, type]); - const nodeExists = useAppSelector((s) => Boolean(s.nodes.present.nodes.find((n) => n.id === nodeId))); + const selectNodeExists = useMemo( + () => createSelector(selectNodes, (nodes) => Boolean(nodes.find((n) => n.id === nodeId))), + [nodeId] + ); + const nodeExists = useAppSelector(selectNodeExists); if (!nodeExists) { return null; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx index f4b6be0cd6..3166236c63 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/InvocationFieldCheck.tsx @@ -2,8 +2,8 @@ import { Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNode } from 'features/nodes/store/selectors'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectInvocationNode, selectNodesSlice } from 'features/nodes/store/selectors'; import type { PropsWithChildren } from 'react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx index 72b60bcee9..d540717166 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/nodes/Invocation/fields/inputs/T5EncoderModelFieldInputComponent.tsx @@ -3,6 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; import { fieldT5EncoderValueChanged } from 'features/nodes/store/nodesSlice'; import type { T5EncoderModelFieldInputInstance, T5EncoderModelFieldInputTemplate } from 'features/nodes/types/field'; +import { selectIsModelsTabDisabled } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { useT5EncoderModels } from 'services/api/hooks/modelsByType'; @@ -15,7 +16,7 @@ type Props = FieldComponentProps { const { nodeId, field } = props; const { t } = useTranslation(); - const disabledTabs = useAppSelector((s) => s.config.disabledTabs); + const isModelsTabDisabled = useAppSelector(selectIsModelsTabDisabled); const dispatch = useAppDispatch(); const [modelConfigs, { isLoading }] = useT5EncoderModels(); const _onChange = useCallback( @@ -42,7 +43,7 @@ const T5EncoderModelFieldInputComponent = (props: Props) => { return ( - + { const dispatch = useAppDispatch(); - const opacity = useAppSelector((s) => s.workflowSettings.nodeOpacity); + const opacity = useAppSelector(selectNodeOpacity); const { onCloseGlobal } = useGlobalMenuClose(); const handleClick = useCallback( (e: MouseEvent) => { if (!e.ctrlKey && !e.altKey && !e.metaKey && !e.shiftKey) { - const { nodes } = store.getState().nodes.present; + const nodes = selectNodes(store.getState()); const nodeChanges: NodeChange[] = []; nodes.forEach(({ id, selected }) => { if (selected !== (id === nodeId)) { diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx index 7a46782f1b..4496e65781 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/NodeOpacitySlider.tsx @@ -1,12 +1,12 @@ import { CompositeSlider, Flex } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { nodeOpacityChanged } from 'features/nodes/store/workflowSettingsSlice'; +import { nodeOpacityChanged, selectNodeOpacity } from 'features/nodes/store/workflowSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const NodeOpacitySlider = () => { const dispatch = useAppDispatch(); - const nodeOpacity = useAppSelector((s) => s.workflowSettings.nodeOpacity); + const nodeOpacity = useAppSelector(selectNodeOpacity); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx index 3d44e9d47b..56d74c31db 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/BottomLeftPanel/ViewportControls.tsx @@ -1,6 +1,9 @@ import { ButtonGroup, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { shouldShowMinimapPanelChanged } from 'features/nodes/store/workflowSettingsSlice'; +import { + selectShouldShowMinimapPanel, + shouldShowMinimapPanelChanged, +} from 'features/nodes/store/workflowSettingsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { @@ -15,7 +18,7 @@ const ViewportControls = () => { const { t } = useTranslation(); const { zoomIn, zoomOut, fitView } = useReactFlow(); const dispatch = useAppDispatch(); - const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel); + const shouldShowMinimapPanel = useAppSelector(selectShouldShowMinimapPanel); const handleClickedZoomIn = useCallback(() => { zoomIn({ duration: 300 }); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx index 92668c3fa8..4afe5dcafa 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/MinimapPanel/MinimapPanel.tsx @@ -1,6 +1,7 @@ import type { SystemStyleObject } from '@invoke-ai/ui-library'; import { chakra, Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectShouldShowMinimapPanel } from 'features/nodes/store/workflowSettingsSlice'; import { memo } from 'react'; import { MiniMap } from 'reactflow'; @@ -16,7 +17,7 @@ const minimapStyles: SystemStyleObject = { }; const MinimapPanel = () => { - const shouldShowMinimapPanel = useAppSelector((s) => s.workflowSettings.shouldShowMinimapPanel); + const shouldShowMinimapPanel = useAppSelector(selectShouldShowMinimapPanel); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx index 7ceb991bd8..0804fe02ff 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/ClearFlowButton.tsx @@ -1,6 +1,7 @@ import { ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; +import { selectWorkflowIsTouched } from 'features/nodes/store/workflowSlice'; import { toast } from 'features/toast/toast'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -10,7 +11,7 @@ const ClearFlowButton = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); - const isTouched = useAppSelector((s) => s.workflow.isTouched); + const isTouched = useAppSelector(selectWorkflowIsTouched); const handleNewWorkflow = useCallback(() => { dispatch(nodeEditorReset()); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx index 3f47d07f8d..557dc6c289 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton.tsx @@ -1,6 +1,7 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; +import { selectWorkflowIsTouched } from 'features/nodes/store/workflowSlice'; import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog'; import { isWorkflowWithID, useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow'; import { memo, useCallback } from 'react'; @@ -9,7 +10,7 @@ import { PiFloppyDiskBold } from 'react-icons/pi'; const SaveWorkflowButton = () => { const { t } = useTranslation(); - const isTouched = useAppSelector((s) => s.workflow.isTouched); + const isTouched = useAppSelector(selectWorkflowIsTouched); const { onOpen } = useSaveWorkflowAsDialog(); const { saveWorkflow } = useSaveLibraryWorkflow(); diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx index 93856a21c4..d1256dade6 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopPanel/TopPanel.tsx @@ -5,11 +5,12 @@ import ClearFlowButton from 'features/nodes/components/flow/panels/TopPanel/Clea import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; import UpdateNodesButton from 'features/nodes/components/flow/panels/TopPanel/UpdateNodesButton'; import { WorkflowName } from 'features/nodes/components/sidePanel/WorkflowName'; +import { selectWorkflowName } from 'features/nodes/store/workflowSlice'; import WorkflowLibraryMenu from 'features/workflowLibrary/components/WorkflowLibraryMenu/WorkflowLibraryMenu'; import { memo } from 'react'; const TopCenterPanel = () => { - const name = useAppSelector((s) => s.workflow.name); + const name = useAppSelector(selectWorkflowName); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx index 37fac8ee7b..5a3f810306 100644 --- a/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/flow/panels/TopRightPanel/WorkflowEditorSettings.tsx @@ -16,12 +16,16 @@ import { Switch, useDisclosure, } from '@invoke-ai/ui-library'; -import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import ReloadNodeTemplatesButton from 'features/nodes/components/flow/panels/TopRightPanel/ReloadSchemaButton'; import { selectionModeChanged, - selectWorkflowSettingsSlice, + selectSelectionMode, + selectShouldAnimateEdges, + selectShouldColorEdges, + selectShouldShouldValidateGraph, + selectShouldShowEdgeLabels, + selectShouldSnapToGrid, shouldAnimateEdgesChanged, shouldColorEdgesChanged, shouldShowEdgeLabelsChanged, @@ -35,25 +39,6 @@ import { SelectionMode } from 'reactflow'; const formLabelProps: FormLabelProps = { flexGrow: 1 }; -const selector = createMemoizedSelector(selectWorkflowSettingsSlice, (workflowSettings) => { - const { - shouldAnimateEdges, - shouldValidateGraph, - shouldSnapToGrid, - shouldColorEdges, - shouldShowEdgeLabels, - selectionMode, - } = workflowSettings; - return { - shouldAnimateEdges, - shouldValidateGraph, - shouldSnapToGrid, - shouldColorEdges, - shouldShowEdgeLabels, - selectionModeIsChecked: selectionMode === SelectionMode.Full, - }; -}); - type Props = { children: (props: { onOpen: () => void }) => ReactNode; }; @@ -61,14 +46,13 @@ type Props = { const WorkflowEditorSettings = ({ children }: Props) => { const { isOpen, onOpen, onClose } = useDisclosure(); const dispatch = useAppDispatch(); - const { - shouldAnimateEdges, - shouldValidateGraph, - shouldSnapToGrid, - shouldColorEdges, - shouldShowEdgeLabels, - selectionModeIsChecked, - } = useAppSelector(selector); + + const shouldSnapToGrid = useAppSelector(selectShouldSnapToGrid); + const selectionMode = useAppSelector(selectSelectionMode); + const shouldColorEdges = useAppSelector(selectShouldColorEdges); + const shouldAnimateEdges = useAppSelector(selectShouldAnimateEdges); + const shouldShowEdgeLabels = useAppSelector(selectShouldShowEdgeLabels); + const shouldValidateGraph = useAppSelector(selectShouldShouldValidateGraph); const handleChangeShouldValidate = useCallback( (e: ChangeEvent) => { @@ -154,7 +138,7 @@ const WorkflowEditorSettings = ({ children }: Props) => { {t('nodes.fullyContainNodes')} - + {t('nodes.fullyContainNodesHelp')} diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx index 555070d673..d1af8e7a74 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/ModeToggle.tsx @@ -1,13 +1,13 @@ import { Flex, IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { workflowModeChanged } from 'features/nodes/store/workflowSlice'; +import { selectWorkflowMode, workflowModeChanged } from 'features/nodes/store/workflowSlice'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiEyeBold, PiPencilBold } from 'react-icons/pi'; export const ModeToggle = () => { const dispatch = useAppDispatch(); - const mode = useAppSelector((s) => s.workflow.mode); + const mode = useAppSelector(selectWorkflowMode); const { t } = useTranslation(); const onClickEdit = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx index 16e007cda7..0fd1bb6ea7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/NodeEditorPanelGroup.tsx @@ -2,6 +2,7 @@ import 'reactflow/dist/style.css'; import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowMode } from 'features/nodes/store/workflowSlice'; import QueueControls from 'features/queue/components/QueueControls'; import ResizeHandle from 'features/ui/components/tabs/ResizeHandle'; import { usePanelStorage } from 'features/ui/hooks/usePanelStorage'; @@ -20,7 +21,7 @@ import { WorkflowName } from './WorkflowName'; const panelGroupStyles: CSSProperties = { height: '100%', width: '100%' }; const NodeEditorPanelGroup = () => { - const mode = useAppSelector((s) => s.workflow.mode); + const mode = useAppSelector(selectWorkflowMode); const panelGroupRef = useRef(null); const panelStorage = usePanelStorage(); diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx index 4c4b17bce1..610900362f 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowMenu.tsx @@ -1,12 +1,13 @@ import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import SaveWorkflowButton from 'features/nodes/components/flow/panels/TopPanel/SaveWorkflowButton'; +import { selectWorkflowMode } from 'features/nodes/store/workflowSlice'; import { NewWorkflowButton } from 'features/workflowLibrary/components/NewWorkflowButton'; import { ModeToggle } from './ModeToggle'; export const WorkflowMenu = () => { - const mode = useAppSelector((s) => s.workflow.mode); + const mode = useAppSelector(selectWorkflowMode); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx index b983e12e11..35a900d644 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/WorkflowName.tsx @@ -1,5 +1,6 @@ import { Flex, Icon, Text, Tooltip } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowIsTouched, selectWorkflowMode, selectWorkflowName } from 'features/nodes/store/workflowSlice'; import { useTranslation } from 'react-i18next'; import { PiDotOutlineFill } from 'react-icons/pi'; @@ -8,9 +9,9 @@ import { WorkflowWarning } from './viewMode/WorkflowWarning'; export const WorkflowName = () => { const { t } = useTranslation(); - const name = useAppSelector((s) => s.workflow.name); - const isTouched = useAppSelector((s) => s.workflow.isTouched); - const mode = useAppSelector((s) => s.workflow.mode); + const name = useAppSelector(selectWorkflowName); + const isTouched = useAppSelector(selectWorkflowIsTouched); + const mode = useAppSelector(selectWorkflowMode); return ( diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx index af0ea710d6..b82a48ea85 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDataTab.tsx @@ -2,8 +2,7 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectLastSelectedNode } from 'features/nodes/store/selectors'; +import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx index f38fa819dd..3ed2189ff7 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorDetailsTab.tsx @@ -6,8 +6,8 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import NotesTextarea from 'features/nodes/components/flow/nodes/Invocation/NotesTextarea'; import { useNodeNeedsUpdate } from 'features/nodes/hooks/useNodeNeedsUpdate'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectLastSelectedNode } from 'features/nodes/store/selectors'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx index b0c6d778b8..048276f5c0 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorOutputsTab.tsx @@ -6,8 +6,8 @@ import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; import { useExecutionState } from 'features/nodes/hooks/useExecutionState'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectLastSelectedNode } from 'features/nodes/store/selectors'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx index d95b215dd6..37956bd64b 100644 --- a/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/sidePanel/inspector/InspectorTemplateTab.tsx @@ -3,8 +3,8 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; import DataViewer from 'features/gallery/components/ImageMetadataViewer/DataViewer'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectLastSelectedNode } from 'features/nodes/store/selectors'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectLastSelectedNode, selectNodesSlice } from 'features/nodes/store/selectors'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts index 0bca73731e..f2cc8690bb 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnection.ts @@ -9,6 +9,7 @@ import { $templates, edgesChanged, } from 'features/nodes/store/nodesSlice'; +import { selectNodes, selectNodesSlice } from 'features/nodes/store/selectors'; import { getFirstValidConnection } from 'features/nodes/store/util/getFirstValidConnection'; import { connectionToEdge } from 'features/nodes/store/util/reactFlowUtil'; import { useCallback, useMemo } from 'react'; @@ -24,7 +25,7 @@ export const useConnection = () => { const onConnectStart = useCallback( (event, { nodeId, handleId, handleType }) => { assert(nodeId && handleId && handleType, 'Invalid connection start event'); - const nodes = store.getState().nodes.present.nodes; + const nodes = selectNodes(store.getState()); const node = nodes.find((n) => n.id === nodeId); if (!node) { @@ -72,7 +73,7 @@ export const useConnection = () => { if (!pendingConnection) { return; } - const { nodes, edges } = store.getState().nodes.present; + const { nodes, edges } = selectNodesSlice(store.getState()); if (mouseOverNodeId) { const { handleType } = pendingConnection; const source = handleType === 'source' ? pendingConnection.nodeId : mouseOverNodeId; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts index 64bb72c54e..4ed2c358c5 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useConnectionState.ts @@ -1,7 +1,8 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $edgePendingUpdate, $pendingConnection, $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $edgePendingUpdate, $pendingConnection, $templates } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { makeConnectionErrorSelector } from 'features/nodes/store/util/makeConnectionErrorSelector'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts index 2a2bfde904..63b892e998 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useCopyPaste.ts @@ -7,8 +7,8 @@ import { $edgesToCopiedNodes, edgesChanged, nodesChanged, - selectNodesSlice, } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { findUnoccupiedPosition } from 'features/nodes/store/util/findUnoccupiedPosition'; import { isEqual, uniqWith } from 'lodash-es'; import type { EdgeChange, NodeChange } from 'reactflow'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts index 5051eaa55b..29befda260 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useDoesInputHaveValue.ts @@ -1,7 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeData } from 'features/nodes/store/selectors'; +import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useDoesInputHaveValue = (nodeId: string, fieldName: string): boolean => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts b/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts index 0e5dc1ac43..a8755e67c4 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useExecutionState.ts @@ -2,7 +2,7 @@ import { useStore } from '@nanostores/react'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; import { deepClone } from 'common/util/deepClone'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { NodeExecutionStates } from 'features/nodes/store/types'; import type { NodeExecutionState } from 'features/nodes/types/invocation'; import { zNodeStatus } from 'features/nodes/types/invocation'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts index 25065e7aba..37bc108a95 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldInputInstance.ts @@ -1,7 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputInstance } from 'features/nodes/store/selectors'; +import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors'; import type { FieldInputInstance } from 'features/nodes/types/field'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts index 92eab8d1b1..d7a7fe48f2 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldLabel.ts @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectFieldInputInstance } from 'features/nodes/store/selectors'; +import { selectFieldInputInstance, selectNodesSlice } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useFieldLabel = (nodeId: string, fieldName: string): string | null => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts index a7e1911720..1e1c7a4dd8 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldTemplate.ts @@ -1,8 +1,8 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNodeType } from 'features/nodes/store/selectors'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectInvocationNodeType, selectNodesSlice } from 'features/nodes/store/selectors'; import type { FieldInputTemplate, FieldOutputTemplate } from 'features/nodes/types/field'; import { useMemo } from 'react'; import { assert } from 'tsafe'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts b/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts index 5b58a7a345..7dfe27bcc5 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useFieldValue.ts @@ -1,6 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts index adf724bdcd..ef4da5bf86 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useGetNodesNeedUpdate.ts @@ -1,7 +1,8 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { getNeedsUpdate } from 'features/nodes/util/node/nodeUpdate'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts index 3fad0a2a86..d46d7148c0 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsIntermediate.ts @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeData } from 'features/nodes/store/selectors'; +import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useIsIntermediate = (nodeId: string): boolean => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts index 9a978b09a8..e1121e08dc 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useIsValidConnection.ts @@ -2,7 +2,9 @@ import { useStore } from '@nanostores/react'; import { useAppSelector, useAppStore } from 'app/store/storeHooks'; import { $edgePendingUpdate, $templates } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { validateConnection } from 'features/nodes/store/util/validateConnection'; +import { selectShouldShouldValidateGraph } from 'features/nodes/store/workflowSettingsSlice'; import { useCallback } from 'react'; import type { Connection } from 'reactflow'; @@ -14,7 +16,7 @@ import type { Connection } from 'reactflow'; export const useIsValidConnection = () => { const store = useAppStore(); const templates = useStore($templates); - const shouldValidateGraph = useAppSelector((s) => s.workflowSettings.shouldValidateGraph); + const shouldValidateGraph = useAppSelector(selectShouldShouldValidateGraph); const isValidConnection = useCallback( ({ source, sourceHandle, target, targetHandle }: Connection): boolean => { // Connection must have valid targets @@ -22,7 +24,7 @@ export const useIsValidConnection = () => { return false; } const edgePendingUpdate = $edgePendingUpdate.get(); - const { nodes, edges } = store.getState().nodes.present; + const { nodes, edges } = selectNodesSlice(store.getState()); const validationResult = validateConnection( { source, sourceHandle, target, targetHandle }, diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts index 738cf80aba..95a2c0d064 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeData.ts @@ -1,7 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeData } from 'features/nodes/store/selectors'; +import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; import type { InvocationNodeData } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts index 56e77a39e8..cd589d6240 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeLabel.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useNodeLabel = (nodeId: string) => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts index 5c920866e9..b9fa480c1a 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodePack.ts @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeData } from 'features/nodes/store/selectors'; +import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useNodePack = (nodeId: string): string | null => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts index 8b076ade1f..403aa9d814 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplate.ts @@ -1,8 +1,8 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectInvocationNodeType } from 'features/nodes/store/selectors'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectInvocationNodeType, selectNodesSlice } from 'features/nodes/store/selectors'; import type { InvocationTemplate } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; import { assert } from 'tsafe'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts index 39ae617460..9855424ee3 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useNodeTemplateTitle.ts @@ -1,7 +1,8 @@ import { useStore } from '@nanostores/react'; import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { $templates, selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { $templates } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { isInvocationNode } from 'features/nodes/types/invocation'; import { useMemo } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts b/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts index aaca80039b..dadf8f72f5 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useUseCache.ts @@ -1,7 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; -import { selectNodeData } from 'features/nodes/store/selectors'; +import { selectNodeData, selectNodesSlice } from 'features/nodes/store/selectors'; import { useMemo } from 'react'; export const useUseCache = (nodeId: string) => { diff --git a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts index 5d79c15442..b33b59c2dd 100644 --- a/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts +++ b/invokeai/frontend/web/src/features/nodes/hooks/useWorkflowWatcher.ts @@ -1,6 +1,6 @@ import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import { selectWorkflowSlice } from 'features/nodes/store/workflowSlice'; import type { WorkflowV3 } from 'features/nodes/types/workflow'; import type { BuildWorkflowArg } from 'features/nodes/util/workflow/buildWorkflow'; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts index 8306225764..d0b144dfa2 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesSlice.ts @@ -1,6 +1,6 @@ import type { PayloadAction, UnknownAction } from '@reduxjs/toolkit'; import { createSlice, isAnyOf } from '@reduxjs/toolkit'; -import type { PersistConfig, RootState } from 'app/store/store'; +import type { PersistConfig } from 'app/store/store'; import { workflowLoaded } from 'features/nodes/store/actions'; import { SHARED_NODE_PROPERTIES } from 'features/nodes/types/constants'; import type { @@ -452,8 +452,6 @@ export const openAddNodePopover = () => { $isAddNodePopoverOpen.set(true); }; -export const selectNodesSlice = (state: RootState) => state.nodes.present; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateNodesState = (state: any): any => { if (!('_version' in state)) { diff --git a/invokeai/frontend/web/src/features/nodes/store/selectors.ts b/invokeai/frontend/web/src/features/nodes/store/selectors.ts index be8cfafa8b..bea0bdfb2f 100644 --- a/invokeai/frontend/web/src/features/nodes/store/selectors.ts +++ b/invokeai/frontend/web/src/features/nodes/store/selectors.ts @@ -1,3 +1,6 @@ +import type { Selector } from '@reduxjs/toolkit'; +import { createSelector } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; import type { NodesState } from 'features/nodes/store/types'; import type { FieldInputInstance } from 'features/nodes/types/field'; import type { InvocationNode, InvocationNodeData } from 'features/nodes/types/invocation'; @@ -36,3 +39,17 @@ export const selectLastSelectedNode = (nodesSlice: NodesState) => { } return null; }; + +export const selectNodesSlice = (state: RootState) => state.nodes.present; + +const createNodesSelector = (selector: Selector) => createSelector(selectNodesSlice, selector); +export const selectNodes = createNodesSelector((nodes) => nodes.nodes); +export const selectEdges = createNodesSelector((nodes) => nodes.edges); +export const selectMayUndo = createSelector( + (state: RootState) => state.nodes, + (nodes) => nodes.past.length > 0 +); +export const selectMayRedo = createSelector( + (state: RootState) => state.nodes, + (nodes) => nodes.future.length > 0 +); diff --git a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts index ec607c60c5..823aa92321 100644 --- a/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts +++ b/invokeai/frontend/web/src/features/nodes/store/util/makeConnectionErrorSelector.ts @@ -1,6 +1,6 @@ import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import type { RootState } from 'app/store/store'; -import { selectNodesSlice } from 'features/nodes/store/nodesSlice'; +import { selectNodesSlice } from 'features/nodes/store/selectors'; import type { NodesState, PendingConnection, Templates } from 'features/nodes/store/types'; import { buildRejectResult, validateConnection } from 'features/nodes/store/util/validateConnection'; import type { Edge, HandleType } from 'reactflow'; diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts index 4a2e45abde..f6deefcf68 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSettingsSlice.ts @@ -1,6 +1,7 @@ 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 { Selector } from 'react-redux'; import { SelectionMode } from 'reactflow'; type WorkflowSettingsState = { @@ -69,8 +70,6 @@ export const { selectionModeChanged, } = workflowSettingsSlice.actions; -export const selectWorkflowSettingsSlice = (state: RootState) => state.workflowSettings; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateWorkflowSettingsState = (state: any): any => { if (!('_version' in state)) { @@ -85,3 +84,15 @@ export const workflowSettingsPersistConfig: PersistConfig migrate: migrateWorkflowSettingsState, persistDenylist: [], }; + +export const selectWorkflowSettingsSlice = (state: RootState) => state.workflowSettings; +const createWorkflowSettingsSelector = (selector: Selector) => + createSelector(selectWorkflowSettingsSlice, selector); +export const selectShouldSnapToGrid = createWorkflowSettingsSelector((s) => s.shouldSnapToGrid); +export const selectSelectionMode = createWorkflowSettingsSelector((s) => s.selectionMode); +export const selectShouldColorEdges = createWorkflowSettingsSelector((s) => s.shouldColorEdges); +export const selectShouldAnimateEdges = createWorkflowSettingsSelector((s) => s.shouldAnimateEdges); +export const selectShouldShowEdgeLabels = createWorkflowSettingsSelector((s) => s.shouldShowEdgeLabels); +export const selectNodeOpacity = createWorkflowSettingsSelector((s) => s.nodeOpacity); +export const selectShouldShowMinimapPanel = createWorkflowSettingsSelector((s) => s.shouldShowMinimapPanel); +export const selectShouldShouldValidateGraph = createWorkflowSettingsSelector((s) => s.shouldValidateGraph); diff --git a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts index 0d358f56e4..2efb1f2216 100644 --- a/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts +++ b/invokeai/frontend/web/src/features/nodes/store/workflowSlice.ts @@ -1,5 +1,5 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { deepClone } from 'common/util/deepClone'; import { workflowLoaded } from 'features/nodes/store/actions'; @@ -209,8 +209,6 @@ export const { workflowSaved, } = workflowSlice.actions; -export const selectWorkflowSlice = (state: RootState) => state.workflow; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateWorkflowState = (state: any): any => { if (!('_version' in state)) { @@ -225,3 +223,12 @@ export const workflowPersistConfig: PersistConfig = { migrate: migrateWorkflowState, persistDenylist: [], }; + +export const selectWorkflowSlice = (state: RootState) => state.workflow; +const createWorkflowSelector = (selector: Selector) => + createSelector(selectWorkflowSlice, selector); + +export const selectWorkflowName = createWorkflowSelector((workflow) => workflow.name); +export const selectWorkflowId = createWorkflowSelector((workflow) => workflow.id); +export const selectWorkflowMode = createWorkflowSelector((workflow) => workflow.mode); +export const selectWorkflowIsTouched = createWorkflowSelector((workflow) => workflow.isTouched); diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx index c81f518b20..c601e3b9b6 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamCFGRescaleMultiplier.tsx @@ -1,19 +1,14 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCfgRescaleMultiplier } from 'features/controlLayers/store/paramsSlice'; +import { selectCFGRescaleMultiplier, setCfgRescaleMultiplier } from 'features/controlLayers/store/paramsSlice'; +import { selectCFGRescaleMultiplierConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCFGRescaleMultiplier = () => { - const cfgRescaleMultiplier = useAppSelector((s) => s.params.cfgRescaleMultiplier); - const initial = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.initial); - const sliderMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.cfgRescaleMultiplier.fineStep); + const cfgRescaleMultiplier = useAppSelector(selectCFGRescaleMultiplier); + const config = useAppSelector(selectCFGRescaleMultiplierConfig); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -27,21 +22,21 @@ const ParamCFGRescaleMultiplier = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx index 5605b81d86..1b437fb22f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Advanced/ParamClipSkip.tsx @@ -1,19 +1,16 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setClipSkip } from 'features/controlLayers/store/paramsSlice'; +import { selectCLIPSKip, selectModel, setClipSkip } from 'features/controlLayers/store/paramsSlice'; import { CLIP_SKIP_MAP } from 'features/parameters/types/constants'; +import { selectCLIPSkipConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamClipSkip = () => { - const clipSkip = useAppSelector((s) => s.params.clipSkip); - const initial = useAppSelector((s) => s.config.sd.clipSkip.initial); - const sliderMin = useAppSelector((s) => s.config.sd.clipSkip.sliderMin); - const numberInputMin = useAppSelector((s) => s.config.sd.clipSkip.numberInputMin); - const coarseStep = useAppSelector((s) => s.config.sd.clipSkip.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.clipSkip.fineStep); - const model = useAppSelector((s) => s.params.model); + const clipSkip = useAppSelector(selectCLIPSKip); + const config = useAppSelector(selectCLIPSkipConfig); + const model = useAppSelector(selectModel); const dispatch = useAppDispatch(); const { t } = useTranslation(); @@ -50,21 +47,21 @@ const ParamClipSkip = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx index 998a8fe05a..007b2b0488 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceEdgeSize.tsx @@ -1,20 +1,15 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceEdgeSize } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasCoherenceEdgeSize, setCanvasCoherenceEdgeSize } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasCoherenceEdgeSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceEdgeSize = () => { const dispatch = useAppDispatch(); - const canvasCoherenceEdgeSize = useAppSelector((s) => s.params.canvasCoherenceEdgeSize); - const initial = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.initial); - const sliderMin = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.canvasCoherenceEdgeSize.fineStep); + const canvasCoherenceEdgeSize = useAppSelector(selectCanvasCoherenceEdgeSize); + const config = useAppSelector(selectCanvasCoherenceEdgeSizeConfig); const { t } = useTranslation(); @@ -31,22 +26,22 @@ const ParamCanvasCoherenceEdgeSize = () => { {t('parameters.coherenceEdgeSize')} diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx index 0a55596ad5..eb9047fbf5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMinDenoise.tsx @@ -1,13 +1,16 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCanvasCoherenceMinDenoise } from 'features/controlLayers/store/paramsSlice'; +import { + selectCanvasCoherenceMinDenoise, + setCanvasCoherenceMinDenoise, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMinDenoise = () => { const dispatch = useAppDispatch(); - const canvasCoherenceMinDenoise = useAppSelector((s) => s.params.canvasCoherenceMinDenoise); + const canvasCoherenceMinDenoise = useAppSelector(selectCanvasCoherenceMinDenoise); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx index 266798a6c9..057c7223c3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/CoherencePass/ParamCanvasCoherenceMode.tsx @@ -2,14 +2,14 @@ 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 { setCanvasCoherenceMode } from 'features/controlLayers/store/paramsSlice'; +import { selectCanvasCoherenceMode, setCanvasCoherenceMode } from 'features/controlLayers/store/paramsSlice'; import { isParameterCanvasCoherenceMode } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCanvasCoherenceMode = () => { const dispatch = useAppDispatch(); - const canvasCoherenceMode = useAppSelector((s) => s.params.canvasCoherenceMode); + const canvasCoherenceMode = useAppSelector(selectCanvasCoherenceMode); const { t } = useTranslation(); const options = useMemo( diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx index 911b084764..a165388fdc 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/Compositing/MaskAdjustment/ParamMaskBlur.tsx @@ -1,21 +1,16 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setMaskBlur } from 'features/controlLayers/store/paramsSlice'; +import { selectMaskBlur, setMaskBlur } from 'features/controlLayers/store/paramsSlice'; +import { selectMaskBlurConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamMaskBlur = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const maskBlur = useAppSelector((s) => s.params.maskBlur); - const initial = useAppSelector((s) => s.config.sd.maskBlur.initial); - const sliderMin = useAppSelector((s) => s.config.sd.maskBlur.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.maskBlur.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.maskBlur.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.maskBlur.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.maskBlur.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.maskBlur.fineStep); + const maskBlur = useAppSelector(selectMaskBlur); + const config = useAppSelector(selectMaskBlurConfig); const handleChange = useCallback( (v: number) => { @@ -30,23 +25,23 @@ const ParamMaskBlur = () => { {t('parameters.maskBlur')} ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx index 90b802d5b6..19aeb133a0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillColorOptions.tsx @@ -1,7 +1,11 @@ import { Box, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIColorPicker from 'common/components/IAIColorPicker'; -import { setInfillColorValue } from 'features/controlLayers/store/paramsSlice'; +import { + selectInfillColorValue, + selectInfillMethod, + setInfillColorValue, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import type { RgbaColor } from 'react-colorful'; import { useTranslation } from 'react-i18next'; @@ -9,8 +13,8 @@ import { useTranslation } from 'react-i18next'; const ParamInfillColorOptions = () => { const dispatch = useAppDispatch(); - const infillColor = useAppSelector((s) => s.params.infillColorValue); - const infillMethod = useAppSelector((s) => s.params.infillMethod); + const infillColor = useAppSelector(selectInfillColorValue); + const infillMethod = useAppSelector(selectInfillMethod); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx index 070a3e224a..2ae24fdb80 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillMethod.tsx @@ -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 { setInfillMethod } from 'features/controlLayers/store/paramsSlice'; +import { selectInfillMethod, setInfillMethod } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; @@ -10,7 +10,7 @@ import { useGetAppConfigQuery } from 'services/api/endpoints/appInfo'; const ParamInfillMethod = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const infillMethod = useAppSelector((s) => s.params.infillMethod); + const infillMethod = useAppSelector(selectInfillMethod); const { data: appConfigData } = useGetAppConfigQuery(); const options = useMemo( () => diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx index 1a5cb1e82a..71cba27c55 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillOptions.tsx @@ -1,4 +1,5 @@ import { useAppSelector } from 'app/store/storeHooks'; +import { selectInfillMethod } from 'features/controlLayers/store/paramsSlice'; import { memo } from 'react'; import ParamInfillColorOptions from './ParamInfillColorOptions'; @@ -6,7 +7,7 @@ import ParamInfillPatchmatchDownscaleSize from './ParamInfillPatchmatchDownscale import ParamInfillTilesize from './ParamInfillTilesize'; const ParamInfillOptions = () => { - const infillMethod = useAppSelector((s) => s.params.infillMethod); + const infillMethod = useAppSelector(selectInfillMethod); if (infillMethod === 'tile') { return ; } diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx index c227b6f954..f2998b9f84 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillPatchmatchDownscaleSize.tsx @@ -1,21 +1,20 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setInfillPatchmatchDownscaleSize } from 'features/controlLayers/store/paramsSlice'; +import { + selectInfillMethod, + selectInfillPatchmatchDownscaleSize, + setInfillPatchmatchDownscaleSize, +} from 'features/controlLayers/store/paramsSlice'; +import { selectInfillPatchmatchDownscaleSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillPatchmatchDownscaleSize = () => { const dispatch = useAppDispatch(); - const infillMethod = useAppSelector((s) => s.params.infillMethod); - const infillPatchmatchDownscaleSize = useAppSelector((s) => s.params.infillPatchmatchDownscaleSize); - const initial = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.initial); - const sliderMin = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.infillPatchmatchDownscaleSize.fineStep); + const infillMethod = useAppSelector(selectInfillMethod); + const infillPatchmatchDownscaleSize = useAppSelector(selectInfillPatchmatchDownscaleSize); + const config = useAppSelector(selectInfillPatchmatchDownscaleSizeConfig); const { t } = useTranslation(); @@ -32,23 +31,23 @@ const ParamInfillPatchmatchDownscaleSize = () => { {t('parameters.patchmatchDownScaleSize')} ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx index 3590bab284..3df4b3e928 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/InfillAndScaling/ParamInfillTilesize.tsx @@ -1,21 +1,15 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setInfillTileSize } from 'features/controlLayers/store/paramsSlice'; +import { selectInfillMethod, selectInfillTileSize, setInfillTileSize } from 'features/controlLayers/store/paramsSlice'; +import { selectInfillTileSizeConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamInfillTileSize = () => { const dispatch = useAppDispatch(); - const infillTileSize = useAppSelector((s) => s.params.infillTileSize); - const initial = useAppSelector((s) => s.config.sd.infillTileSize.initial); - const sliderMin = useAppSelector((s) => s.config.sd.infillTileSize.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.infillTileSize.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.infillTileSize.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.infillTileSize.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.infillTileSize.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.infillTileSize.fineStep); - - const infillMethod = useAppSelector((s) => s.params.infillMethod); + const infillTileSize = useAppSelector(selectInfillTileSize); + const config = useAppSelector(selectInfillTileSizeConfig); + const infillMethod = useAppSelector(selectInfillMethod); const { t } = useTranslation(); @@ -30,23 +24,23 @@ const ParamInfillTileSize = () => { {t('parameters.tileSize')} ); diff --git a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx index f09e62d1ea..c250b4a7a0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Canvas/ParamImageToImageStrength.tsx @@ -1,10 +1,10 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; +import { selectImg2imgStrength, setImg2imgStrength } from 'features/controlLayers/store/paramsSlice'; import ImageToImageStrength from 'features/parameters/components/ImageToImage/ImageToImageStrength'; import { memo, useCallback } from 'react'; const ParamImageToImageStrength = () => { - const img2imgStrength = useAppSelector((s) => s.params.img2imgStrength); + const img2imgStrength = useAppSelector(selectImg2imgStrength); const dispatch = useAppDispatch(); const onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx index 8465d94cd5..145ca6f2da 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamCFGScale.tsx @@ -1,22 +1,20 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setCfgScale } from 'features/controlLayers/store/paramsSlice'; +import { selectCFGScale, setCfgScale } from 'features/controlLayers/store/paramsSlice'; +import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamCFGScale = () => { - const cfgScale = useAppSelector((s) => s.params.cfgScale); - const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.guidance.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.guidance.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.guidance.fineStep); - const initial = useAppSelector((s) => s.config.sd.guidance.initial); + const cfgScale = useAppSelector(selectCFGScale); + const config = useAppSelector(selectCFGScaleConfig); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + const marks = useMemo( + () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], + [config.sliderMax, config.sliderMin] + ); const onChange = useCallback((v: number) => dispatch(setCfgScale(v)), [dispatch]); return ( @@ -26,21 +24,21 @@ const ParamCFGScale = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx index 0117383a7b..49f9d0fbfd 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamNegativePrompt.tsx @@ -1,21 +1,25 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePromptChanged } from 'features/controlLayers/store/paramsSlice'; +import { negativePromptChanged, selectNegativePrompt } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { ViewModePrompt } from 'features/parameters/components/Prompts/ViewModePrompt'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; +import { + selectStylePresetActivePresetId, + selectStylePresetViewMode, +} from 'features/stylePresets/store/stylePresetSlice'; import { memo, useCallback, useRef } from 'react'; import { useTranslation } from 'react-i18next'; import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamNegativePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.params.negativePrompt); - const viewMode = useAppSelector((s) => s.stylePreset.viewMode); - const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); + const prompt = useAppSelector(selectNegativePrompt); + const viewMode = useAppSelector(selectStylePresetViewMode); + const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); const { activeStylePreset } = useListStylePresetsQuery(undefined, { selectFromResult: ({ data }) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx index b5659e19bf..cc3f9a86be 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamPositivePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePromptChanged } from 'features/controlLayers/store/paramsSlice'; +import { positivePromptChanged, selectBase, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; import { ShowDynamicPromptsPreviewButton } from 'features/dynamicPrompts/components/ShowDynamicPromptsPreviewButton'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; @@ -9,6 +9,10 @@ import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; import { PromptPopover } from 'features/prompt/PromptPopover'; import { usePrompt } from 'features/prompt/usePrompt'; import { SDXLConcatButton } from 'features/sdxl/components/SDXLPrompts/SDXLConcatButton'; +import { + selectStylePresetActivePresetId, + selectStylePresetViewMode, +} from 'features/stylePresets/store/stylePresetSlice'; import { memo, useCallback, useRef } from 'react'; import type { HotkeyCallback } from 'react-hotkeys-hook'; import { useHotkeys } from 'react-hotkeys-hook'; @@ -17,10 +21,10 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const ParamPositivePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.params.positivePrompt); - const baseModel = useAppSelector((s) => s.params.model)?.base; - const viewMode = useAppSelector((s) => s.stylePreset.viewMode); - const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); + const prompt = useAppSelector(selectPositivePrompt); + const baseModel = useAppSelector(selectBase); + const viewMode = useAppSelector(selectStylePresetViewMode); + const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); const { activeStylePreset } = useListStylePresetsQuery(undefined, { selectFromResult: ({ data }) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx index 3fa62e6735..d670de68b8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamScheduler.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } 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 { setScheduler } from 'features/controlLayers/store/paramsSlice'; +import { selectScheduler, setScheduler } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; const ParamScheduler = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const scheduler = useAppSelector((s) => s.params.scheduler); + const scheduler = useAppSelector(selectScheduler); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx index 582f80e37c..f7ef4660b5 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Core/ParamSteps.tsx @@ -1,22 +1,20 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSteps } from 'features/controlLayers/store/paramsSlice'; +import { selectSteps, setSteps } from 'features/controlLayers/store/paramsSlice'; +import { selectStepsConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSteps = () => { - const steps = useAppSelector((s) => s.params.steps); - const initial = useAppSelector((s) => s.config.sd.steps.initial); - const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.steps.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.steps.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.steps.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.steps.fineStep); + const steps = useAppSelector(selectSteps); + const config = useAppSelector(selectStepsConfig); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + const marks = useMemo( + () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], + [config.sliderMax, config.sliderMin] + ); const onChange = useCallback( (v: number) => { dispatch(setSteps(v)); @@ -31,21 +29,21 @@ const ParamSteps = () => { diff --git a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx index c56fef1903..59e24d4278 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ImageToImage/ImageToImageStrength.tsx @@ -1,6 +1,7 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; +import { selectImg2imgStrengthConfig } from 'features/system/store/configSlice'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,13 +13,7 @@ type Props = { }; const ImageToImageStrength = ({ value, onChange }: Props) => { - const initial = useAppSelector((s) => s.config.sd.img2imgStrength.initial); - const sliderMin = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.img2imgStrength.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.img2imgStrength.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.img2imgStrength.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.img2imgStrength.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.img2imgStrength.fineStep); + const config = useAppSelector(selectImg2imgStrengthConfig); const { t } = useTranslation(); return ( @@ -27,23 +22,23 @@ const ImageToImageStrength = ({ value, onChange }: Props) => { {`${t('parameters.denoisingStrength')}`} ); diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx index 268674d8c3..67e1cdd670 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/NavigateToModelManagerButton.tsx @@ -1,22 +1,22 @@ import type { IconButtonProps } from '@invoke-ai/ui-library'; import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectIsModelsTabDisabled } from 'features/system/store/configSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; -import { memo, useCallback, useMemo } from 'react'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiGearSixBold } from 'react-icons/pi'; export const NavigateToModelManagerButton = memo((props: Omit) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const disabledTabs = useAppSelector((s) => s.config.disabledTabs); - const shouldShowButton = useMemo(() => !disabledTabs.includes('models'), [disabledTabs]); + const isModelsTabDisabled = useAppSelector(selectIsModelsTabDisabled); const handleClick = useCallback(() => { dispatch(setActiveTab('models')); }, [dispatch]); - if (!shouldShowButton) { + if (isModelsTabDisabled) { return null; } diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx index bcc56d2e40..86445a9ca0 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/ParamMainModelSelect.tsx @@ -2,6 +2,7 @@ import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-li import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; +import { selectModel } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { modelSelected } from 'features/parameters/store/actions'; import { memo, useCallback, useMemo } from 'react'; @@ -12,7 +13,7 @@ import type { MainModelConfig } from 'services/api/types'; const ParamMainModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const selectedModel = useAppSelector((s) => s.params.model); + const selectedModel = useAppSelector(selectModel); const [modelConfigs, { isLoading }] = useSDMainModels(); const tooltipLabel = useMemo(() => { if (!modelConfigs.length || !selectedModel) { diff --git a/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx index 5eb3cfb2d0..1a60aa8f13 100644 --- a/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/MainModel/UseDefaultSettingsButton.tsx @@ -1,12 +1,13 @@ import { IconButton } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectModel } from 'features/controlLayers/store/paramsSlice'; import { setDefaultSettings } from 'features/parameters/store/actions'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { RiSparklingFill } from 'react-icons/ri'; export const UseDefaultSettingsButton = () => { - const model = useAppSelector((s) => s.params.model); + const model = useAppSelector(selectModel); const { t } = useTranslation(); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx index 35a4b04211..58eb99faf8 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/ParamPostProcessingModel.tsx @@ -1,7 +1,7 @@ import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { postProcessingModelChanged } from 'features/parameters/store/upscaleSlice'; +import { postProcessingModelChanged, selectPostProcessingModel } from 'features/parameters/store/upscaleSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSpandrelImageToImageModels } from 'services/api/hooks/modelsByType'; @@ -11,7 +11,7 @@ const ParamPostProcessingModel = () => { const { t } = useTranslation(); const [modelConfigs, { isLoading }] = useSpandrelImageToImageModels(); - const model = useAppSelector((s) => s.upscale.postProcessingModel); + const model = useAppSelector(selectPostProcessingModel); const dispatch = useAppDispatch(); const tooltipLabel = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx index e9092fbee8..2b8b335994 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/PostProcessing/PostProcessingPopover.tsx @@ -13,6 +13,7 @@ import { adHocPostProcessingRequested } from 'app/store/middleware/listenerMiddl import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; import ParamPostProcessingModel from 'features/parameters/components/PostProcessing/ParamPostProcessingModel'; +import { selectPostProcessingModel } from 'features/parameters/store/upscaleSlice'; import { useIsQueueMutationInProgress } from 'features/queue/hooks/useIsQueueMutationInProgress'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { memo, useCallback } from 'react'; @@ -25,7 +26,7 @@ type Props = { imageDTO?: ImageDTO }; export const PostProcessingPopover = memo((props: Props) => { const { imageDTO } = props; const dispatch = useAppDispatch(); - const { postProcessingModel } = useAppSelector((s) => s.upscale); + const postProcessingModel = useAppSelector(selectPostProcessingModel); const inProgress = useIsQueueMutationInProgress(); const { t } = useTranslation(); const { isOpen, onOpen, onClose } = useDisclosure(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx index 3bec31c64c..04bace0f1c 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Prompts/Prompts.tsx @@ -1,17 +1,20 @@ import { Flex } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { createParamsSelector } from 'features/controlLayers/store/paramsSlice'; import { ParamNegativePrompt } from 'features/parameters/components/Core/ParamNegativePrompt'; import { ParamPositivePrompt } from 'features/parameters/components/Core/ParamPositivePrompt'; import { ParamSDXLNegativeStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt'; import { ParamSDXLPositiveStylePrompt } from 'features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt'; import { memo } from 'react'; +const selectWithStylePrompts = createParamsSelector((params) => { + const isSDXL = params.model?.base === 'sdxl'; + const shouldConcatPrompts = params.shouldConcatPrompts; + return isSDXL && !shouldConcatPrompts; +}); + export const Prompts = memo(() => { - const withStylePrompts = useAppSelector((s) => { - const isSDXL = s.params.model?.base === 'sdxl'; - const shouldConcatPrompts = s.params.shouldConcatPrompts; - return isSDXL && !shouldConcatPrompts; - }); + const withStylePrompts = useAppSelector(selectWithStylePrompts); return ( diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx index 338feaa1e4..aced3654a1 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessXAxis.tsx @@ -1,14 +1,14 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeamlessXAxis } from 'features/controlLayers/store/paramsSlice'; +import { selectSeamlessXAxis, setSeamlessXAxis } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSeamlessXAxis = () => { const { t } = useTranslation(); - const seamlessXAxis = useAppSelector((s) => s.params.seamlessXAxis); + const seamlessXAxis = useAppSelector(selectSeamlessXAxis); const dispatch = useAppDispatch(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx index 9b75f76798..ca84094da3 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seamless/ParamSeamlessYAxis.tsx @@ -1,14 +1,14 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeamlessYAxis } from 'features/controlLayers/store/paramsSlice'; +import { selectSeamlessYAxis, setSeamlessYAxis } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSeamlessYAxis = () => { const { t } = useTranslation(); - const seamlessYAxis = useAppSelector((s) => s.params.seamlessYAxis); + const seamlessYAxis = useAppSelector(selectSeamlessYAxis); const dispatch = useAppDispatch(); const handleChange = useCallback( (e: ChangeEvent) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx index 149e5135ae..33d714d4ea 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedNumberInput.tsx @@ -2,13 +2,13 @@ import { CompositeNumberInput, FormControl, FormLabel } from '@invoke-ai/ui-libr import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setSeed } from 'features/controlLayers/store/paramsSlice'; +import { selectSeed, selectShouldRandomizeSeed, setSeed } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; export const ParamSeedNumberInput = memo(() => { - const seed = useAppSelector((s) => s.params.seed); - const shouldRandomizeSeed = useAppSelector((s) => s.params.shouldRandomizeSeed); + const seed = useAppSelector(selectSeed); + const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx index c25f5c0fe9..bd523089d4 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedRandomize.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { setShouldRandomizeSeed } from 'features/controlLayers/store/paramsSlice'; +import { selectShouldRandomizeSeed, setShouldRandomizeSeed } from 'features/controlLayers/store/paramsSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ export const ParamSeedRandomize = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const shouldRandomizeSeed = useAppSelector((s) => s.params.shouldRandomizeSeed); + const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); const handleChangeShouldRandomizeSeed = useCallback( (e: ChangeEvent) => dispatch(setShouldRandomizeSeed(e.target.checked)), diff --git a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx index 24820bb614..5223a5d06f 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Seed/ParamSeedShuffle.tsx @@ -2,14 +2,14 @@ import { Button } from '@invoke-ai/ui-library'; import { NUMPY_RAND_MAX, NUMPY_RAND_MIN } from 'app/constants'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import randomInt from 'common/util/randomInt'; -import { setSeed } from 'features/controlLayers/store/paramsSlice'; +import { selectShouldRandomizeSeed, setSeed } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { PiShuffleBold } from 'react-icons/pi'; export const ParamSeedShuffle = memo(() => { const dispatch = useAppDispatch(); - const shouldRandomizeSeed = useAppSelector((s) => s.params.shouldRandomizeSeed); + const shouldRandomizeSeed = useAppSelector(selectShouldRandomizeSeed); const { t } = useTranslation(); const handleClickRandomizeSeed = useCallback( diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamCreativity.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamCreativity.tsx index 0304a732cb..a83e952963 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamCreativity.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamCreativity.tsx @@ -1,22 +1,23 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { creativityChanged } from 'features/parameters/store/upscaleSlice'; -import { memo, useCallback, useMemo } from 'react'; +import { creativityChanged, selectCreativity } from 'features/parameters/store/upscaleSlice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const initial = 0; +const sliderMin = -10; +const sliderMax = 10; +const numberInputMin = -10; +const numberInputMax = 10; +const coarseStep = 1; +const fineStep = 1; +const marks = [sliderMin, 0, sliderMax]; + const ParamCreativity = () => { - const creativity = useAppSelector((s) => s.upscale.creativity); - const initial = 0; - const sliderMin = -10; - const sliderMax = 10; - const numberInputMin = -10; - const numberInputMax = 10; - const coarseStep = 1; - const fineStep = 1; + const creativity = useAppSelector(selectCreativity); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, 0, sliderMax], [sliderMax, sliderMin]); const onChange = useCallback( (v: number) => { dispatch(creativityChanged(v)); diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx index 501245d77b..7bb9d03d27 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamSpandrelModel.tsx @@ -2,7 +2,7 @@ import { Box, Combobox, FormControl, FormLabel, Tooltip } from '@invoke-ai/ui-li import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; +import { selectUpscaleModel, upscaleModelChanged } from 'features/parameters/store/upscaleSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { useSpandrelImageToImageModels } from 'services/api/hooks/modelsByType'; @@ -12,7 +12,7 @@ const ParamSpandrelModel = () => { const { t } = useTranslation(); const [modelConfigs, { isLoading }] = useSpandrelImageToImageModels(); - const model = useAppSelector((s) => s.upscale.upscaleModel); + const model = useAppSelector(selectUpscaleModel); const dispatch = useAppDispatch(); const tooltipLabel = useMemo(() => { diff --git a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamStructure.tsx b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamStructure.tsx index d3cd85c846..1beb4a192b 100644 --- a/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamStructure.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/Upscale/ParamStructure.tsx @@ -1,22 +1,23 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { structureChanged } from 'features/parameters/store/upscaleSlice'; -import { memo, useCallback, useMemo } from 'react'; +import { selectStructure, structureChanged } from 'features/parameters/store/upscaleSlice'; +import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; +const initial = 0; +const sliderMin = -10; +const sliderMax = 10; +const numberInputMin = -10; +const numberInputMax = 10; +const coarseStep = 1; +const fineStep = 1; +const marks = [sliderMin, 0, sliderMax]; + const ParamStructure = () => { - const structure = useAppSelector((s) => s.upscale.structure); - const initial = 0; - const sliderMin = -10; - const sliderMax = 10; - const numberInputMin = -10; - const numberInputMax = 10; - const coarseStep = 1; - const fineStep = 1; + const structure = useAppSelector(selectStructure); const dispatch = useAppDispatch(); const { t } = useTranslation(); - const marks = useMemo(() => [sliderMin, 0, sliderMax], [sliderMax, sliderMin]); const onChange = useCallback( (v: number) => { dispatch(structureChanged(v)); diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx index cf7008a3ea..3544bfe9bd 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEModelSelect.tsx @@ -2,7 +2,7 @@ import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useGroupedModelCombobox } from 'common/hooks/useGroupedModelCombobox'; -import { vaeSelected } from 'features/controlLayers/store/paramsSlice'; +import { selectBase, selectVAE, vaeSelected } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,16 +12,16 @@ import type { VAEModelConfig } from 'services/api/types'; const ParamVAEModelSelect = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const model = useAppSelector((s) => s.params.model); - const vae = useAppSelector((s) => s.params.vae); + const base = useAppSelector(selectBase); + const vae = useAppSelector(selectVAE); const [modelConfigs, { isLoading }] = useVAEModels(); const getIsDisabled = useCallback( (vae: VAEModelConfig): boolean => { - const isCompatible = model?.base === vae.base; - const hasMainModel = Boolean(model?.base); + const isCompatible = base === vae.base; + const hasMainModel = Boolean(base); return !hasMainModel || !isCompatible; }, - [model?.base] + [base] ); const _onChange = useCallback( (vae: VAEModelConfig | null) => { diff --git a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx index 4677b97e0f..aaafa894e4 100644 --- a/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/VAEModel/ParamVAEPrecision.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } 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 { vaePrecisionChanged } from 'features/controlLayers/store/paramsSlice'; +import { selectVAEPrecision, vaePrecisionChanged } from 'features/controlLayers/store/paramsSlice'; import { isParameterPrecision } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -15,7 +15,7 @@ const options = [ const ParamVAEModelSelect = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const vaePrecision = useAppSelector((s) => s.params.vaePrecision); + const vaePrecision = useAppSelector(selectVAEPrecision); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts b/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts index f047e7d3f5..e5bf4951ae 100644 --- a/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts +++ b/invokeai/frontend/web/src/features/parameters/hooks/useIsTooLargeToUpscale.ts @@ -5,7 +5,7 @@ import { selectConfigSlice } from 'features/system/store/configSlice'; import { useMemo } from 'react'; import type { ImageDTO } from 'services/api/types'; -const createIsTooLargeToUpscaleSelector = (imageDTO?: ImageDTO) => +const createIsTooLargeToUpscaleSelector = (imageDTO?: ImageDTO | null) => createMemoizedSelector(selectUpscalelice, selectConfigSlice, (upscale, config) => { const { upscaleModel, scale } = upscale; const { maxUpscaleDimension } = config; @@ -23,7 +23,7 @@ const createIsTooLargeToUpscaleSelector = (imageDTO?: ImageDTO) => return upscaledPixels > maxPixels; }); -export const useIsTooLargeToUpscale = (imageDTO?: ImageDTO) => { +export const useIsTooLargeToUpscale = (imageDTO?: ImageDTO | null) => { const selectIsTooLargeToUpscale = useMemo(() => createIsTooLargeToUpscaleSelector(imageDTO), [imageDTO]); return useAppSelector(selectIsTooLargeToUpscale); }; diff --git a/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts b/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts index 0ea4523a15..bc1230dc99 100644 --- a/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/upscaleSlice.ts @@ -1,5 +1,5 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import type { ParameterSpandrelImageToImageModel } from 'features/parameters/types/parameterSchemas'; import type { ControlNetModelConfig, ImageDTO } from 'services/api/types'; @@ -64,8 +64,6 @@ export const { postProcessingModelChanged, } = upscaleSlice.actions; -export const selectUpscalelice = (state: RootState) => state.upscale; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateUpscaleState = (state: any): any => { if (!('_version' in state)) { @@ -80,3 +78,13 @@ export const upscalePersistConfig: PersistConfig = { migrate: migrateUpscaleState, persistDenylist: [], }; + +export const selectUpscalelice = (state: RootState) => state.upscale; +const createUpscaleSelector = (selector: Selector) => createSelector(selectUpscalelice, selector); +export const selectPostProcessingModel = createUpscaleSelector((upscale) => upscale.postProcessingModel); +export const selectCreativity = createUpscaleSelector((upscale) => upscale.creativity); +export const selectUpscaleModel = createUpscaleSelector((upscale) => upscale.upscaleModel); +export const selectTileControlNetModel = createUpscaleSelector((upscale) => upscale.tileControlnetModel); +export const selectStructure = createUpscaleSelector((upscale) => upscale.structure); +export const selectUpscaleInitialImage = createUpscaleSelector((upscale) => upscale.upscaleInitialImage); +export const selectUpscaleScale = createUpscaleSelector((upscale) => upscale.scale); diff --git a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx index b4b14f4e52..ef122f7d19 100644 --- a/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx +++ b/invokeai/frontend/web/src/features/prompt/PromptTriggerSelect.tsx @@ -3,6 +3,8 @@ import { Combobox, FormControl } from '@invoke-ai/ui-library'; import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; import type { GroupBase } from 'chakra-react-select'; +import { selectAddedLoRAs } from 'features/controlLayers/store/lorasSlice'; +import { selectModel } from 'features/controlLayers/store/paramsSlice'; import type { PromptTriggerSelectProps } from 'features/prompt/types'; import { t } from 'i18next'; import { flatten, map } from 'lodash-es'; @@ -17,8 +19,8 @@ const noOptionsMessage = () => t('prompt.noMatchingTriggers'); export const PromptTriggerSelect = memo(({ onSelect, onClose }: PromptTriggerSelectProps) => { const { t } = useTranslation(); - const mainModel = useAppSelector((s) => s.params.model); - const addedLoRAs = useAppSelector((s) => s.loras.loras); + const mainModel = useAppSelector(selectModel); + const addedLoRAs = useAppSelector(selectAddedLoRAs); const { data: mainModelConfig, isLoading: isLoadingMainModelConfig } = useGetModelConfigQuery( mainModel?.key ?? skipToken ); diff --git a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx index 52dc5e24af..b37ce12304 100644 --- a/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx +++ b/invokeai/frontend/web/src/features/queue/components/InvokeQueueBackButton.tsx @@ -1,5 +1,6 @@ import { Button, Flex, Spacer } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectDynamicPromptsIsLoading } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { QueueIterationsNumberInput } from 'features/queue/components/QueueIterationsNumberInput'; import { useQueueBack } from 'features/queue/hooks/useQueueBack'; import { memo } from 'react'; @@ -11,7 +12,7 @@ const invoke = 'Invoke'; export const InvokeQueueBackButton = memo(() => { const { queueBack, isLoading, isDisabled } = useQueueBack(); - const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); + const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); return ( diff --git a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx index 7fbd97f6d5..b49b3fedd7 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueButtonTooltip.tsx @@ -2,9 +2,13 @@ import { Divider, Flex, ListItem, Text, Tooltip, UnorderedList } from '@invoke-a import { createSelector } from '@reduxjs/toolkit'; import { useAppSelector } from 'app/store/storeHooks'; import { useIsReadyToEnqueue } from 'common/hooks/useIsReadyToEnqueue'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; -import { selectDynamicPromptsSlice } from 'features/dynamicPrompts/store/dynamicPromptsSlice'; +import { selectIterations, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { + selectDynamicPromptsIsLoading, + selectDynamicPromptsSlice, +} from 'features/dynamicPrompts/store/dynamicPromptsSlice'; import { getShouldProcessPrompt } from 'features/dynamicPrompts/util/getShouldProcessPrompt'; +import { selectAutoAddBoardId } from 'features/gallery/store/gallerySelectors'; import type { PropsWithChildren } from 'react'; import { memo, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -30,10 +34,10 @@ export const QueueButtonTooltip = (props: PropsWithChildren) => { const TooltipContent = memo(({ prepend = false }: Props) => { const { t } = useTranslation(); const { isReady, reasons } = useIsReadyToEnqueue(); - const isLoadingDynamicPrompts = useAppSelector((s) => s.dynamicPrompts.isLoading); + const isLoadingDynamicPrompts = useAppSelector(selectDynamicPromptsIsLoading); const promptsCount = useAppSelector(selectPromptsCount); - const iterationsCount = useAppSelector((s) => s.params.iterations); - const autoAddBoardId = useAppSelector((s) => s.gallery.autoAddBoardId); + const iterationsCount = useAppSelector(selectIterations); + const autoAddBoardId = useAppSelector(selectAutoAddBoardId); const autoAddBoardName = useBoardName(autoAddBoardId); const [_, { isLoading }] = useEnqueueBatchMutation({ fixedCacheKey: 'enqueueBatch', diff --git a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx index a9a0a055a1..ee37f7cde8 100644 --- a/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx +++ b/invokeai/frontend/web/src/features/queue/components/QueueIterationsNumberInput.tsx @@ -1,13 +1,13 @@ import { CompositeNumberInput } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setIterations } from 'features/controlLayers/store/paramsSlice'; +import { selectIterations, setIterations } from 'features/controlLayers/store/paramsSlice'; +import { selectIterationsConfig } from 'features/system/store/configSlice'; import { memo, useCallback } from 'react'; export const QueueIterationsNumberInput = memo(() => { - const iterations = useAppSelector((s) => s.params.iterations); - const coarseStep = useAppSelector((s) => s.config.sd.iterations.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.iterations.fineStep); + const iterations = useAppSelector(selectIterations); + const config = useAppSelector(selectIterationsConfig); const dispatch = useAppDispatch(); const handleChange = useCallback( (v: number) => { @@ -19,8 +19,8 @@ export const QueueIterationsNumberInput = memo(() => { return ( = (index, item, ); const QueueList = () => { - const listCursor = useAppSelector((s) => s.queue.listCursor); - const listPriority = useAppSelector((s) => s.queue.listPriority); + const listCursor = useAppSelector(selectQueueListCursor); + const listPriority = useAppSelector(selectQueueListPriority); const dispatch = useAppDispatch(); const rootRef = useRef(null); const [scroller, setScroller] = useState(null); diff --git a/invokeai/frontend/web/src/features/queue/store/queueSlice.ts b/invokeai/frontend/web/src/features/queue/store/queueSlice.ts index 38c1cf5349..9f0225149d 100644 --- a/invokeai/frontend/web/src/features/queue/store/queueSlice.ts +++ b/invokeai/frontend/web/src/features/queue/store/queueSlice.ts @@ -1,5 +1,6 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; +import type { RootState } from 'app/store/store'; interface QueueState { listCursor: number | undefined; @@ -33,3 +34,8 @@ export const queueSlice = createSlice({ }); export const { listCursorChanged, listPriorityChanged, listParamsReset } = queueSlice.actions; + +const selectQueueSlice = (state: RootState) => state.queue; +const createQueueSelector = (selector: Selector) => createSelector(selectQueueSlice, selector); +export const selectQueueListCursor = createQueueSelector((queue) => queue.listCursor); +export const selectQueueListPriority = createQueueSelector((queue) => queue.listPriority); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx index 0bd542e0db..32f504db2e 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLNegativeStylePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { negativePrompt2Changed } from 'features/controlLayers/store/paramsSlice'; +import { negativePrompt2Changed, selectNegativePrompt2 } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; @@ -12,7 +12,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLNegativeStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.params.negativePrompt2); + const prompt = useAppSelector(selectNegativePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx index a73bf76d90..5290ac959c 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/ParamSDXLPositiveStylePrompt.tsx @@ -1,6 +1,6 @@ import { Box, Textarea } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { positivePrompt2Changed } from 'features/controlLayers/store/paramsSlice'; +import { positivePrompt2Changed, selectPositivePrompt2 } from 'features/controlLayers/store/paramsSlice'; import { PromptLabel } from 'features/parameters/components/Prompts/PromptLabel'; import { PromptOverlayButtonWrapper } from 'features/parameters/components/Prompts/PromptOverlayButtonWrapper'; import { AddPromptTriggerButton } from 'features/prompt/AddPromptTriggerButton'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; export const ParamSDXLPositiveStylePrompt = memo(() => { const dispatch = useAppDispatch(); - const prompt = useAppSelector((s) => s.params.positivePrompt2); + const prompt = useAppSelector(selectPositivePrompt2); const textareaRef = useRef(null); const { t } = useTranslation(); const handleChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx index 3cde1edc64..79a23e3717 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLPrompts/SDXLConcatButton.tsx @@ -1,12 +1,12 @@ import { IconButton, Tooltip } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { shouldConcatPromptsChanged } from 'features/controlLayers/store/paramsSlice'; +import { selectShouldConcatPrompts, shouldConcatPromptsChanged } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { PiLinkSimpleBold, PiLinkSimpleBreakBold } from 'react-icons/pi'; export const SDXLConcatButton = memo(() => { - const shouldConcatPrompts = useAppSelector((s) => s.params.shouldConcatPrompts); + const shouldConcatPrompts = useAppSelector(selectShouldConcatPrompts); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx index af4ebdabb8..3d0ad822ba 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale.tsx @@ -1,22 +1,20 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerCFGScale } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerCFGScale, setRefinerCFGScale } from 'features/controlLayers/store/paramsSlice'; +import { selectCFGScaleConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerCFGScale = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const refinerCFGScale = useAppSelector((s) => s.params.refinerCFGScale); - const sliderMin = useAppSelector((s) => s.config.sd.guidance.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.guidance.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.guidance.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.guidance.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.guidance.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.guidance.fineStep); - const initial = useAppSelector((s) => s.config.sd.guidance.initial); - const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + const refinerCFGScale = useAppSelector(selectRefinerCFGScale); + const config = useAppSelector(selectCFGScaleConfig); + const marks = useMemo( + () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], + [config.sliderMax, config.sliderMin] + ); const onChange = useCallback((v: number) => dispatch(setRefinerCFGScale(v)), [dispatch]); @@ -27,21 +25,21 @@ const ParamSDXLRefinerCFGScale = () => { diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx index e54a8329b9..e9f05d20ad 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect.tsx @@ -2,7 +2,7 @@ import { Box, Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import { useModelCombobox } from 'common/hooks/useModelCombobox'; -import { refinerModelChanged } from 'features/controlLayers/store/paramsSlice'; +import { refinerModelChanged, selectRefinerModel } from 'features/controlLayers/store/paramsSlice'; import { zModelIdentifierField } from 'features/nodes/types/common'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -13,7 +13,7 @@ const optionsFilter = (model: MainModelConfig) => model.base === 'sdxl-refiner'; const ParamSDXLRefinerModelSelect = () => { const dispatch = useAppDispatch(); - const model = useAppSelector((s) => s.params.refinerModel); + const model = useAppSelector(selectRefinerModel); const { t } = useTranslation(); const [modelConfigs, { isLoading }] = useRefinerModels(); const _onChange = useCallback( diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx index f378acd942..3729468bcd 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore.tsx @@ -1,12 +1,15 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerNegativeAestheticScore } from 'features/controlLayers/store/paramsSlice'; +import { + selectRefinerNegativeAestheticScore, + setRefinerNegativeAestheticScore, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerNegativeAestheticScore = () => { - const refinerNegativeAestheticScore = useAppSelector((s) => s.params.refinerNegativeAestheticScore); + const refinerNegativeAestheticScore = useAppSelector(selectRefinerNegativeAestheticScore); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx index bc92f5a178..2661c5ce57 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerPositiveAestheticScore.tsx @@ -1,12 +1,15 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerPositiveAestheticScore } from 'features/controlLayers/store/paramsSlice'; +import { + selectRefinerPositiveAestheticScore, + setRefinerPositiveAestheticScore, +} from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerPositiveAestheticScore = () => { - const refinerPositiveAestheticScore = useAppSelector((s) => s.params.refinerPositiveAestheticScore); + const refinerPositiveAestheticScore = useAppSelector(selectRefinerPositiveAestheticScore); const dispatch = useAppDispatch(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx index f2b63f6542..acce9dd8e9 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerScheduler.tsx @@ -2,7 +2,7 @@ import type { ComboboxOnChange } 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 { setRefinerScheduler } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerScheduler, setRefinerScheduler } from 'features/controlLayers/store/paramsSlice'; import { SCHEDULER_OPTIONS } from 'features/parameters/types/constants'; import { isParameterScheduler } from 'features/parameters/types/parameterSchemas'; import { memo, useCallback, useMemo } from 'react'; @@ -11,7 +11,7 @@ import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerScheduler = () => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const refinerScheduler = useAppSelector((s) => s.params.refinerScheduler); + const refinerScheduler = useAppSelector(selectRefinerScheduler); const onChange = useCallback( (v) => { diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx index c5024d5eca..856ca39134 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart.tsx @@ -1,12 +1,12 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerStart } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerStart, setRefinerStart } from 'features/controlLayers/store/paramsSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerStart = () => { - const refinerStart = useAppSelector((s) => s.params.refinerStart); + const refinerStart = useAppSelector(selectRefinerStart); const dispatch = useAppDispatch(); const handleChange = useCallback((v: number) => dispatch(setRefinerStart(v)), [dispatch]); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx index f24e896a80..3a3a2ce5c4 100644 --- a/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx +++ b/invokeai/frontend/web/src/features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps.tsx @@ -1,23 +1,21 @@ import { CompositeNumberInput, CompositeSlider, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { setRefinerSteps } from 'features/controlLayers/store/paramsSlice'; +import { selectRefinerSteps, setRefinerSteps } from 'features/controlLayers/store/paramsSlice'; +import { selectStepsConfig } from 'features/system/store/configSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; const ParamSDXLRefinerSteps = () => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const refinerSteps = useAppSelector((s) => s.params.refinerSteps); - const initial = useAppSelector((s) => s.config.sd.steps.initial); - const sliderMin = useAppSelector((s) => s.config.sd.steps.sliderMin); - const sliderMax = useAppSelector((s) => s.config.sd.steps.sliderMax); - const numberInputMin = useAppSelector((s) => s.config.sd.steps.numberInputMin); - const numberInputMax = useAppSelector((s) => s.config.sd.steps.numberInputMax); - const coarseStep = useAppSelector((s) => s.config.sd.steps.coarseStep); - const fineStep = useAppSelector((s) => s.config.sd.steps.fineStep); + const refinerSteps = useAppSelector(selectRefinerSteps); + const config = useAppSelector(selectStepsConfig); - const marks = useMemo(() => [sliderMin, Math.floor(sliderMax / 2), sliderMax], [sliderMax, sliderMin]); + const marks = useMemo( + () => [config.sliderMin, Math.floor(config.sliderMax / 2), config.sliderMax], + [config.sliderMax, config.sliderMin] + ); const onChange = useCallback( (v: number) => { @@ -33,21 +31,21 @@ const ParamSDXLRefinerSteps = () => { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx index 2c7616118e..d618a54dae 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/AdvancedSettingsAccordion/AdvancedSettingsAccordion.tsx @@ -3,7 +3,7 @@ import { Flex, FormControlGroup, StandaloneAccordion } from '@invoke-ai/ui-libra import { skipToken } from '@reduxjs/toolkit/query'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectParamsSlice, selectVAEKey } from 'features/controlLayers/store/paramsSlice'; import ParamCFGRescaleMultiplier from 'features/parameters/components/Advanced/ParamCFGRescaleMultiplier'; import ParamClipSkip from 'features/parameters/components/Advanced/ParamClipSkip'; import ParamSeamlessXAxis from 'features/parameters/components/Seamless/ParamSeamlessXAxis'; @@ -28,7 +28,7 @@ const formLabelProps2: FormLabelProps = { }; export const AdvancedSettingsAccordion = memo(() => { - const vaeKey = useAppSelector((state) => state.params.vae?.key); + const vaeKey = useAppSelector(selectVAEKey); const { currentData: vaeConfig } = useGetModelConfigQuery(vaeKey ?? skipToken); const activeTabName = useAppSelector(selectActiveTab); diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx index bb8c5627bf..26b52dfb99 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/RefinerSettingsAccordion/RefinerSettingsAccordion.tsx @@ -2,7 +2,7 @@ import type { FormLabelProps } from '@invoke-ai/ui-library'; import { Flex, FormControlGroup, StandaloneAccordion, Text } from '@invoke-ai/ui-library'; import { createMemoizedSelector } from 'app/store/createMemoizedSelector'; import { useAppSelector } from 'app/store/storeHooks'; -import { selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; +import { selectIsRefinerModelSelected, selectParamsSlice } from 'features/controlLayers/store/paramsSlice'; import ParamSDXLRefinerCFGScale from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerCFGScale'; import ParamSDXLRefinerModelSelect from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerModelSelect'; import ParamSDXLRefinerNegativeAestheticScore from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerNegativeAestheticScore'; @@ -11,7 +11,6 @@ import ParamSDXLRefinerScheduler from 'features/sdxl/components/SDXLRefiner/Para import ParamSDXLRefinerStart from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerStart'; import ParamSDXLRefinerSteps from 'features/sdxl/components/SDXLRefiner/ParamSDXLRefinerSteps'; import { useStandaloneAccordionToggle } from 'features/settingsAccordions/hooks/useStandaloneAccordionToggle'; -import { isNil } from 'lodash-es'; import { memo } from 'react'; import { useTranslation } from 'react-i18next'; import { useIsRefinerAvailable } from 'services/api/hooks/useIsRefinerAvailable'; @@ -60,7 +59,7 @@ const RefinerSettingsAccordionNoRefiner: React.FC = memo(() => { RefinerSettingsAccordionNoRefiner.displayName = 'RefinerSettingsAccordionNoRefiner'; const RefinerSettingsAccordionContent: React.FC = memo(() => { - const isRefinerModelSelected = useAppSelector((state) => !isNil(state.params.refinerModel)); + const isRefinerModelSelected = useAppSelector(selectIsRefinerModelSelected); return ( diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleInitialImage.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleInitialImage.tsx index e3b312b903..9ef1455c7d 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleInitialImage.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleInitialImage.tsx @@ -3,7 +3,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIDndImage from 'common/components/IAIDndImage'; import IAIDndImageIcon from 'common/components/IAIDndImageIcon'; import type { TypesafeDroppableData } from 'features/dnd/types'; -import { upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; +import { selectUpscaleInitialImage, upscaleInitialImageChanged } from 'features/parameters/store/upscaleSlice'; import { t } from 'i18next'; import { useCallback, useMemo } from 'react'; import { PiArrowCounterClockwiseBold } from 'react-icons/pi'; @@ -11,7 +11,7 @@ import type { PostUploadAction } from 'services/api/types'; export const UpscaleInitialImage = () => { const dispatch = useAppDispatch(); - const imageDTO = useAppSelector((s) => s.upscale.upscaleInitialImage); + const imageDTO = useAppSelector(selectUpscaleInitialImage); const droppableData = useMemo( () => ({ diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider.tsx index dd4f46557f..5c39e34c87 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleScaleSlider.tsx @@ -1,7 +1,7 @@ import { CompositeNumberInput, CompositeSlider, Flex, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; -import { scaleChanged } from 'features/parameters/store/upscaleSlice'; +import { scaleChanged, selectUpscaleScale } from 'features/parameters/store/upscaleSlice'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,7 +12,7 @@ const formatValue = (val: string | number) => `${val}x`; export const UpscaleScaleSlider = memo(() => { const dispatch = useAppDispatch(); const { t } = useTranslation(); - const scale = useAppSelector((s) => s.upscale.scale); + const scale = useAppSelector(selectUpscaleScale); const onChange = useCallback( (val: number) => { diff --git a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx index 8a791032b6..757fc27b67 100644 --- a/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx +++ b/invokeai/frontend/web/src/features/settingsAccordions/components/UpscaleSettingsAccordion/UpscaleWarning.tsx @@ -1,8 +1,15 @@ import { Button, Flex, ListItem, Text, UnorderedList } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; +import { selectModel } from 'features/controlLayers/store/paramsSlice'; import { $installModelsTab } from 'features/modelManagerV2/subpanels/InstallModels'; import { useIsTooLargeToUpscale } from 'features/parameters/hooks/useIsTooLargeToUpscale'; -import { tileControlnetModelChanged } from 'features/parameters/store/upscaleSlice'; +import { + selectTileControlNetModel, + selectUpscaleInitialImage, + selectUpscaleModel, + tileControlnetModelChanged, +} from 'features/parameters/store/upscaleSlice'; +import { selectIsModelsTabDisabled, selectMaxUpscaleDimension } from 'features/system/store/configSlice'; import { setActiveTab } from 'features/ui/store/uiSlice'; import { useCallback, useEffect, useMemo } from 'react'; import { Trans, useTranslation } from 'react-i18next'; @@ -10,16 +17,15 @@ import { useControlNetModels } from 'services/api/hooks/modelsByType'; export const UpscaleWarning = () => { const { t } = useTranslation(); - const model = useAppSelector((s) => s.params.model); - const upscaleModel = useAppSelector((s) => s.upscale.upscaleModel); - const tileControlnetModel = useAppSelector((s) => s.upscale.tileControlnetModel); - const upscaleInitialImage = useAppSelector((s) => s.upscale.upscaleInitialImage); + const model = useAppSelector(selectModel); + const upscaleModel = useAppSelector(selectUpscaleModel); + const tileControlnetModel = useAppSelector(selectTileControlNetModel); + const upscaleInitialImage = useAppSelector(selectUpscaleInitialImage); const dispatch = useAppDispatch(); const [modelConfigs, { isLoading }] = useControlNetModels(); - const disabledTabs = useAppSelector((s) => s.config.disabledTabs); - const shouldShowButton = useMemo(() => !disabledTabs.includes('models'), [disabledTabs]); - const maxUpscaleDimension = useAppSelector((s) => s.config.maxUpscaleDimension); - const isTooLargeToUpscale = useIsTooLargeToUpscale(upscaleInitialImage || undefined); + const isModelsTabDisabled = useAppSelector(selectIsModelsTabDisabled); + const maxUpscaleDimension = useAppSelector(selectMaxUpscaleDimension); + const isTooLargeToUpscale = useIsTooLargeToUpscale(upscaleInitialImage); useEffect(() => { const validModel = modelConfigs.find((cnetModel) => { @@ -57,7 +63,7 @@ export const UpscaleWarning = () => { $installModelsTab.set(3); }, [dispatch]); - if (modelWarnings.length && !shouldShowButton) { + if (modelWarnings.length && isModelsTabDisabled) { return null; } diff --git a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx index 60d14bde22..e3cf278edf 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/ActiveStylePreset.tsx @@ -2,7 +2,12 @@ import { Badge, Flex, IconButton, Spacer, Text, Tooltip } from '@invoke-ai/ui-li import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { negativePromptChanged, positivePromptChanged } from 'features/controlLayers/store/paramsSlice'; import { usePresetModifiedPrompts } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; -import { activeStylePresetIdChanged, viewModeChanged } from 'features/stylePresets/store/stylePresetSlice'; +import { + activeStylePresetIdChanged, + selectStylePresetActivePresetId, + selectStylePresetViewMode, + viewModeChanged, +} from 'features/stylePresets/store/stylePresetSlice'; import type { MouseEventHandler } from 'react'; import { useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -12,9 +17,8 @@ import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; import StylePresetImage from './StylePresetImage'; export const ActiveStylePreset = () => { - const viewMode = useAppSelector((s) => s.stylePreset.viewMode); - - const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); + const viewMode = useAppSelector(selectStylePresetViewMode); + const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); const { activeStylePreset } = useListStylePresetsQuery(undefined, { selectFromResult: ({ data }) => { diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetForm.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetForm.tsx index 2fd703103e..aadc82f1fe 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetForm.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetForm/StylePresetForm.tsx @@ -2,6 +2,7 @@ import { Box, Button, Flex, FormControl, FormLabel, Input, Spacer, Text } from ' import { useAppSelector } from 'app/store/storeHooks'; import { PRESET_PLACEHOLDER } from 'features/stylePresets/hooks/usePresetModifiedPrompts'; import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal'; +import { selectAllowPrivateStylePresets } from 'features/system/store/configSlice'; import { toast } from 'features/toast/toast'; import type { PropsWithChildren } from 'react'; import { useCallback } from 'react'; @@ -33,7 +34,7 @@ export const StylePresetForm = ({ const [createStylePreset, { isLoading: isCreating }] = useCreateStylePresetMutation(); const [updateStylePreset, { isLoading: isUpdating }] = useUpdateStylePresetMutation(); const { t } = useTranslation(); - const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets); + const allowPrivateStylePresets = useAppSelector(selectAllowPrivateStylePresets); const { handleSubmit, control, register, formState } = useForm({ defaultValues: formData || { diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx index 5c8e9170eb..9afcbbb807 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetList.tsx @@ -1,6 +1,7 @@ import { Button, Collapse, Flex, Icon, Text, useDisclosure } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { IAINoContentFallback } from 'common/components/IAIImageFallback'; +import { selectStylePresetSearchTerm } from 'features/stylePresets/store/stylePresetSlice'; import { useTranslation } from 'react-i18next'; import { PiCaretDownBold } from 'react-icons/pi'; import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; @@ -10,7 +11,7 @@ import { StylePresetListItem } from './StylePresetListItem'; export const StylePresetList = ({ title, data }: { title: string; data: StylePresetRecordWithImage[] }) => { const { t } = useTranslation(); const { onToggle, isOpen } = useDisclosure({ defaultIsOpen: true }); - const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm); + const searchTerm = useAppSelector(selectStylePresetSearchTerm); return ( diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx index cf98db1ee6..1387b964b8 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetListItem.tsx @@ -2,7 +2,10 @@ import { Badge, ConfirmationAlertDialog, Flex, IconButton, Text, useDisclosure } import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { $isMenuOpen } from 'features/stylePresets/store/isMenuOpen'; import { $stylePresetModalState } from 'features/stylePresets/store/stylePresetModal'; -import { activeStylePresetIdChanged } from 'features/stylePresets/store/stylePresetSlice'; +import { + activeStylePresetIdChanged, + selectStylePresetActivePresetId, +} from 'features/stylePresets/store/stylePresetSlice'; import { toast } from 'features/toast/toast'; import type { MouseEvent } from 'react'; import { useCallback } from 'react'; @@ -16,7 +19,7 @@ import StylePresetImage from './StylePresetImage'; export const StylePresetListItem = ({ preset }: { preset: StylePresetRecordWithImage }) => { const dispatch = useAppDispatch(); const [deleteStylePreset] = useDeleteStylePresetMutation(); - const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); + const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); const { isOpen, onOpen, onClose } = useDisclosure(); const { t } = useTranslation(); diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx index 51a0a92c66..73cc8b2a41 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetMenu.tsx @@ -3,6 +3,8 @@ import { EMPTY_ARRAY } from 'app/store/constants'; import { useAppSelector } from 'app/store/storeHooks'; import { StylePresetExportButton } from 'features/stylePresets/components/StylePresetExportButton'; import { StylePresetImportButton } from 'features/stylePresets/components/StylePresetImportButton'; +import { selectStylePresetSearchTerm } from 'features/stylePresets/store/stylePresetSlice'; +import { selectAllowPrivateStylePresets } from 'features/system/store/configSlice'; import { useTranslation } from 'react-i18next'; import type { StylePresetRecordWithImage } from 'services/api/endpoints/stylePresets'; import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; @@ -12,8 +14,8 @@ import { StylePresetList } from './StylePresetList'; import StylePresetSearch from './StylePresetSearch'; export const StylePresetMenu = () => { - const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm); - const allowPrivateStylePresets = useAppSelector((s) => s.config.allowPrivateStylePresets); + const searchTerm = useAppSelector(selectStylePresetSearchTerm); + const allowPrivateStylePresets = useAppSelector(selectAllowPrivateStylePresets); const { data } = useListStylePresetsQuery(undefined, { selectFromResult: ({ data }) => { const filteredData = diff --git a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetSearch.tsx b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetSearch.tsx index dcb50e8569..8d59500ed6 100644 --- a/invokeai/frontend/web/src/features/stylePresets/components/StylePresetSearch.tsx +++ b/invokeai/frontend/web/src/features/stylePresets/components/StylePresetSearch.tsx @@ -1,6 +1,6 @@ import { IconButton, Input, InputGroup, InputRightElement } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { searchTermChanged } from 'features/stylePresets/store/stylePresetSlice'; +import { searchTermChanged, selectStylePresetSearchTerm } from 'features/stylePresets/store/stylePresetSlice'; import type { ChangeEvent, KeyboardEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -8,7 +8,7 @@ import { PiXBold } from 'react-icons/pi'; const StylePresetSearch = () => { const dispatch = useAppDispatch(); - const searchTerm = useAppSelector((s) => s.stylePreset.searchTerm); + const searchTerm = useAppSelector(selectStylePresetSearchTerm); const { t } = useTranslation(); const handlePresetSearch = useCallback( diff --git a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts index 516121a039..16d5a3502d 100644 --- a/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts +++ b/invokeai/frontend/web/src/features/stylePresets/hooks/usePresetModifiedPrompts.ts @@ -1,4 +1,6 @@ import { useAppSelector } from 'app/store/storeHooks'; +import { selectNegativePrompt, selectPositivePrompt } from 'features/controlLayers/store/paramsSlice'; +import { selectStylePresetActivePresetId } from 'features/stylePresets/store/stylePresetSlice'; import { useListStylePresetsQuery } from 'services/api/endpoints/stylePresets'; export const PRESET_PLACEHOLDER = '{prompt}'; @@ -10,10 +12,9 @@ export const buildPresetModifiedPrompt = (presetPrompt: string, currentPrompt: s }; export const usePresetModifiedPrompts = () => { - const positivePrompt = useAppSelector((s) => s.params.positivePrompt); - const negativePrompt = useAppSelector((s) => s.params.negativePrompt); - - const activeStylePresetId = useAppSelector((s) => s.stylePreset.activeStylePresetId); + const positivePrompt = useAppSelector(selectPositivePrompt); + const negativePrompt = useAppSelector(selectNegativePrompt); + const activeStylePresetId = useAppSelector(selectStylePresetActivePresetId); const { activeStylePreset } = useListStylePresetsQuery(undefined, { selectFromResult: ({ data }) => { diff --git a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts index e7d6a2738c..867177f8f0 100644 --- a/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts +++ b/invokeai/frontend/web/src/features/stylePresets/store/stylePresetSlice.ts @@ -1,4 +1,4 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { stylePresetsApi } from 'services/api/endpoints/stylePresets'; @@ -64,8 +64,12 @@ export const stylePresetPersistConfig: PersistConfig = { persistDenylist: [], }; -export const selectStylePresetSlice = (state: RootState) => state.stylePreset; -export const selectActivePresetId = createSelector( - selectStylePresetSlice, +const selectStylePresetSlice = (state: RootState) => state.stylePreset; +const createStylePresetSelector = (selector: Selector) => + createSelector(selectStylePresetSlice, selector); + +export const selectStylePresetActivePresetId = createStylePresetSelector( (stylePreset) => stylePreset.activeStylePresetId ); +export const selectStylePresetViewMode = createStylePresetSelector((stylePreset) => stylePreset.viewMode); +export const selectStylePresetSearchTerm = createStylePresetSelector((stylePreset) => stylePreset.searchTerm); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled.tsx index 315546d3da..e21d1b59b4 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled.tsx @@ -1,6 +1,6 @@ import { FormControl, FormLabel, Switch } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; -import { logIsEnabledChanged } from 'features/system/store/systemSlice'; +import { logIsEnabledChanged, selectSystemLogIsEnabled } from 'features/system/store/systemSlice'; import type { ChangeEvent } from 'react'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -9,7 +9,7 @@ import { useDispatch } from 'react-redux'; export const SettingsDeveloperLogIsEnabled = memo(() => { const { t } = useTranslation(); const dispatch = useDispatch(); - const logIsEnabled = useAppSelector((s) => s.system.logIsEnabled); + const logIsEnabled = useAppSelector(selectSystemLogIsEnabled); const onChangeLogIsEnabled = useCallback( (e: ChangeEvent) => { diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogLevel.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogLevel.tsx index 06d05b7761..a93ced8664 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogLevel.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogLevel.tsx @@ -2,14 +2,14 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { isLogLevel, zLogLevel } from 'app/logging/logger'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; -import { logLevelChanged } from 'features/system/store/systemSlice'; +import { logLevelChanged, selectSystemLogLevel } from 'features/system/store/systemSlice'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; export const SettingsDeveloperLogLevel = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const logLevel = useAppSelector((s) => s.system.logLevel); + const logLevel = useAppSelector(selectSystemLogLevel); const options = useMemo(() => zLogLevel.options.map((o) => ({ label: t(`system.logLevel.${o}`), value: o })), [t]); const value = useMemo(() => options.find((o) => o.value === logLevel), [logLevel, options]); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogNamespaces.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogNamespaces.tsx index c3e7d58d74..9cb36ca028 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogNamespaces.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsDeveloperLogNamespaces.tsx @@ -1,9 +1,8 @@ import { Flex, FormControl, FormLabel, Tag, TagCloseButton, Text } from '@invoke-ai/ui-library'; import type { LogNamespace } from 'app/logging/logger'; import { zLogNamespace } from 'app/logging/logger'; -import { EMPTY_ARRAY } from 'app/store/constants'; import { useAppSelector } from 'app/store/storeHooks'; -import { logNamespaceToggled } from 'features/system/store/systemSlice'; +import { logNamespaceToggled, selectSystemLogNamespaces } from 'features/system/store/systemSlice'; import { difference } from 'lodash-es'; import { memo, useCallback, useMemo } from 'react'; import { useTranslation } from 'react-i18next'; @@ -11,13 +10,7 @@ import { useDispatch } from 'react-redux'; export const SettingsDeveloperLogNamespaces = memo(() => { const { t } = useTranslation(); - const enabledLogNamespaces = useAppSelector((s) => { - if (s.system.logNamespaces.length === 0) { - return EMPTY_ARRAY; - } else { - return s.system.logNamespaces; - } - }); + const enabledLogNamespaces = useAppSelector(selectSystemLogNamespaces); const disabledLogNamespaces = useMemo( () => difference(zLogNamespace.options, enabledLogNamespaces), [enabledLogNamespaces] diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLanguageSelect.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLanguageSelect.tsx index bee5940530..57d2a5a8ca 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLanguageSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsLanguageSelect.tsx @@ -2,6 +2,7 @@ import type { ComboboxOnChange } from '@invoke-ai/ui-library'; import { Combobox, FormControl, FormLabel } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { useFeatureStatus } from 'features/system/hooks/useFeatureStatus'; +import { selectLanguage } from 'features/system/store/systemSelectors'; import { languageChanged } from 'features/system/store/systemSlice'; import type { Language } from 'features/system/store/types'; import { isLanguage } from 'features/system/store/types'; @@ -39,7 +40,7 @@ const options = map(optionsObject, (label, value) => ({ label, value })); export const SettingsLanguageSelect = memo(() => { const { t } = useTranslation(); const dispatch = useAppDispatch(); - const language = useAppSelector((s) => s.system.language); + const language = useAppSelector(selectLanguage); const isLocalizationEnabled = useFeatureStatus('localization'); const value = useMemo(() => options.find((o) => o.value === language), [language]); diff --git a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx index 9b19616fa1..b9569a1a5c 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -19,7 +19,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { InformationalPopover } from 'common/components/InformationalPopover/InformationalPopover'; import ScrollableContent from 'common/components/OverlayScrollbars/ScrollableContent'; import { useClearStorage } from 'common/hooks/useClearStorage'; -import { shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice'; +import { selectShouldUseCPUNoise, shouldUseCpuNoiseChanged } from 'features/controlLayers/store/paramsSlice'; import { SettingsDeveloperLogIsEnabled } from 'features/system/components/SettingsModal/SettingsDeveloperLogIsEnabled'; import { SettingsDeveloperLogLevel } from 'features/system/components/SettingsModal/SettingsDeveloperLogLevel'; import { SettingsDeveloperLogNamespaces } from 'features/system/components/SettingsModal/SettingsDeveloperLogNamespaces'; @@ -27,12 +27,18 @@ import { useClearIntermediates } from 'features/system/components/SettingsModal/ import { StickyScrollable } from 'features/system/components/StickyScrollable'; import { logIsEnabledChanged, + selectSystemShouldAntialiasProgressImage, + selectSystemShouldConfirmOnDelete, + selectSystemShouldEnableInformationalPopovers, + selectSystemShouldUseNSFWChecker, + selectSystemShouldUseWatermarker, setShouldConfirmOnDelete, setShouldEnableInformationalPopovers, shouldAntialiasProgressImageChanged, shouldUseNSFWCheckerChanged, shouldUseWatermarkerChanged, } from 'features/system/store/systemSlice'; +import { selectShouldShowProgressInViewer } from 'features/ui/store/uiSelectors'; import { setShouldShowProgressInViewer } from 'features/ui/store/uiSlice'; import type { ChangeEvent, ReactElement } from 'react'; import { cloneElement, memo, useCallback, useEffect, useState } from 'react'; @@ -89,13 +95,13 @@ const SettingsModal = ({ children, config }: SettingsModalProps) => { const { isOpen: isRefreshModalOpen, onOpen: onRefreshModalOpen, onClose: onRefreshModalClose } = useDisclosure(); - const shouldUseCpuNoise = useAppSelector((s) => s.params.shouldUseCpuNoise); - const shouldConfirmOnDelete = useAppSelector((s) => s.system.shouldConfirmOnDelete); - const shouldShowProgressInViewer = useAppSelector((s) => s.ui.shouldShowProgressInViewer); - const shouldAntialiasProgressImage = useAppSelector((s) => s.system.shouldAntialiasProgressImage); - const shouldUseNSFWChecker = useAppSelector((s) => s.system.shouldUseNSFWChecker); - const shouldUseWatermarker = useAppSelector((s) => s.system.shouldUseWatermarker); - const shouldEnableInformationalPopovers = useAppSelector((s) => s.system.shouldEnableInformationalPopovers); + const shouldUseCpuNoise = useAppSelector(selectShouldUseCPUNoise); + const shouldConfirmOnDelete = useAppSelector(selectSystemShouldConfirmOnDelete); + const shouldShowProgressInViewer = useAppSelector(selectShouldShowProgressInViewer); + const shouldAntialiasProgressImage = useAppSelector(selectSystemShouldAntialiasProgressImage); + const shouldUseNSFWChecker = useAppSelector(selectSystemShouldUseNSFWChecker); + const shouldUseWatermarker = useAppSelector(selectSystemShouldUseWatermarker); + const shouldEnableInformationalPopovers = useAppSelector(selectSystemShouldEnableInformationalPopovers); const clearStorage = useClearStorage(); diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index 2a387ad696..5a1698556a 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -1,5 +1,5 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { RootState } from 'app/store/store'; import type { AppConfig, NumericalParameterConfig, PartialAppConfig } from 'app/types/invokeai'; import { merge } from 'lodash-es'; @@ -182,3 +182,27 @@ export const configSlice = createSlice({ export const { configChanged } = configSlice.actions; export const selectConfigSlice = (state: RootState) => state.config; +const createConfigSelector = (selector: Selector) => createSelector(selectConfigSlice, selector); + +export const selectWidthConfig = createConfigSelector((config) => config.sd.width); +export const selectHeightConfig = createConfigSelector((config) => config.sd.height); +export const selectStepsConfig = createConfigSelector((config) => config.sd.steps); +export const selectCFGScaleConfig = createConfigSelector((config) => config.sd.guidance); +export const selectCLIPSkipConfig = createConfigSelector((config) => config.sd.clipSkip); +export const selectCFGRescaleMultiplierConfig = createConfigSelector((config) => config.sd.cfgRescaleMultiplier); +export const selectCanvasCoherenceEdgeSizeConfig = createConfigSelector((config) => config.sd.canvasCoherenceEdgeSize); +export const selectMaskBlurConfig = createConfigSelector((config) => config.sd.maskBlur); +export const selectInfillPatchmatchDownscaleSizeConfig = createConfigSelector( + (config) => config.sd.infillPatchmatchDownscaleSize +); +export const selectInfillTileSizeConfig = createConfigSelector((config) => config.sd.infillTileSize); +export const selectImg2imgStrengthConfig = createConfigSelector((config) => config.sd.img2imgStrength); +export const selectMaxPromptsConfig = createConfigSelector((config) => config.sd.dynamicPrompts.maxPrompts); +export const selectIterationsConfig = createConfigSelector((config) => config.sd.iterations); + +export const selectMaxUpscaleDimension = createConfigSelector((config) => config.maxUpscaleDimension); +export const selectAllowPrivateStylePresets = createConfigSelector((config) => config.allowPrivateStylePresets); +export const selectWorkflowFetchDebounce = createConfigSelector((config) => config.workflowFetchDebounce ?? 300); +export const selectMetadataFetchDebounce = createConfigSelector((config) => config.metadataFetchDebounce ?? 300); + +export const selectIsModelsTabDisabled = createConfigSelector((config) => config.disabledTabs.includes('models')); diff --git a/invokeai/frontend/web/src/features/system/store/systemSelectors.ts b/invokeai/frontend/web/src/features/system/store/systemSelectors.ts index 817e2f2b86..d83f494e22 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSelectors.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSelectors.ts @@ -1,4 +1,4 @@ import { createSelector } from '@reduxjs/toolkit'; import { selectSystemSlice } from 'features/system/store/systemSlice'; -export const languageSelector = createSelector(selectSystemSlice, (system) => system.language); +export const selectLanguage = createSelector(selectSystemSlice, (system) => system.language); diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index c10c9635ec..854d5816e8 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -1,7 +1,8 @@ -import type { PayloadAction } from '@reduxjs/toolkit'; -import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction, Selector } from '@reduxjs/toolkit'; +import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { LogNamespace } from 'app/logging/logger'; import { zLogNamespace } from 'app/logging/logger'; +import { EMPTY_ARRAY } from 'app/store/constants'; import type { PersistConfig, RootState } from 'app/store/store'; import { uniq } from 'lodash-es'; @@ -70,8 +71,6 @@ export const { setShouldEnableInformationalPopovers, } = systemSlice.actions; -export const selectSystemSlice = (state: RootState) => state.system; - /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ const migrateSystemState = (state: any): any => { if (!('_version' in state)) { @@ -86,3 +85,21 @@ export const systemPersistConfig: PersistConfig = { migrate: migrateSystemState, persistDenylist: [], }; + +export const selectSystemSlice = (state: RootState) => state.system; +const createSystemSelector = (selector: Selector) => createSelector(selectSystemSlice, selector); + +export const selectSystemLogLevel = createSystemSelector((system) => system.logLevel); +export const selectSystemLogNamespaces = createSystemSelector((system) => + system.logNamespaces.length > 0 ? system.logNamespaces : EMPTY_ARRAY +); +export const selectSystemLogIsEnabled = createSystemSelector((system) => system.logIsEnabled); +export const selectSystemShouldConfirmOnDelete = createSystemSelector((system) => system.shouldConfirmOnDelete); +export const selectSystemShouldUseNSFWChecker = createSystemSelector((system) => system.shouldUseNSFWChecker); +export const selectSystemShouldUseWatermarker = createSystemSelector((system) => system.shouldUseWatermarker); +export const selectSystemShouldAntialiasProgressImage = createSystemSelector( + (system) => system.shouldAntialiasProgressImage +); +export const selectSystemShouldEnableInformationalPopovers = createSystemSelector( + (system) => system.shouldEnableInformationalPopovers +); diff --git a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx index 0c57465404..68844bf082 100644 --- a/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx +++ b/invokeai/frontend/web/src/features/ui/components/ParametersPanels/ParametersPanelTextToImage.tsx @@ -5,6 +5,7 @@ import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { overlayScrollbarsParams } from 'common/components/OverlayScrollbars/constants'; import { CanvasEntityListMenuButton } from 'features/controlLayers/components/CanvasEntityList/CanvasEntityListMenuButton'; import { CanvasPanelContent } from 'features/controlLayers/components/CanvasPanelContent'; +import { selectIsSDXL } from 'features/controlLayers/store/paramsSlice'; import { selectEntityCount } from 'features/controlLayers/store/selectors'; import { isImageViewerOpenChanged } from 'features/gallery/store/gallerySlice'; import { Prompts } from 'features/parameters/components/Prompts/Prompts'; @@ -49,7 +50,7 @@ const ParametersPanelTextToImage = () => { } return `${t('controlLayers.controlLayers')} (${controlLayersCount})`; }, [controlLayersCount, t]); - const isSDXL = useAppSelector((s) => s.params.model?.base === 'sdxl'); + const isSDXL = useAppSelector(selectIsSDXL); const onChangeTabs = useCallback( (i: number) => { if (i === 1) { diff --git a/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx b/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx index f790618ca6..8bda801ffe 100644 --- a/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx +++ b/invokeai/frontend/web/src/features/ui/components/TabVisibilityGate.tsx @@ -1,11 +1,12 @@ import { Box } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectActiveTab } from 'features/ui/store/uiSelectors'; import type { TabName } from 'features/ui/store/uiTypes'; import type { PropsWithChildren } from 'react'; import { memo } from 'react'; export const TabVisibilityGate = memo(({ tab, children }: PropsWithChildren<{ tab: TabName }>) => { - const activeTabName = useAppSelector((s) => s.ui.activeTab); + const activeTabName = useAppSelector(selectActiveTab); return ( { - const mode = useAppSelector((s) => s.workflow.mode); + const mode = useAppSelector(selectWorkflowMode); const activeTabName = useAppSelector(selectActiveTab); const ref = useRef(null); useScopeOnFocus('workflows', ref); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx index 701441b093..704412052a 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/NewWorkflowConfirmationAlertDialog.tsx @@ -1,7 +1,7 @@ import { ConfirmationAlertDialog, Flex, Text, useDisclosure } from '@invoke-ai/ui-library'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import { nodeEditorReset } from 'features/nodes/store/nodesSlice'; -import { workflowModeChanged } from 'features/nodes/store/workflowSlice'; +import { selectWorkflowIsTouched, workflowModeChanged } from 'features/nodes/store/workflowSlice'; import { toast } from 'features/toast/toast'; import { memo, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; @@ -14,7 +14,7 @@ export const NewWorkflowConfirmationAlertDialog = memo((props: Props) => { const { t } = useTranslation(); const dispatch = useAppDispatch(); const { isOpen, onOpen, onClose } = useDisclosure(); - const isTouched = useAppSelector((s) => s.workflow.isTouched); + const isTouched = useAppSelector(selectWorkflowIsTouched); const handleNewWorkflow = useCallback(() => { dispatch(nodeEditorReset()); diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListItem.tsx index 11a9530cad..fcb63f07ae 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryListItem.tsx @@ -1,6 +1,8 @@ import { Button, Flex, Heading, Spacer, Text } from '@invoke-ai/ui-library'; +import { EMPTY_OBJECT } from 'app/store/constants'; import { useAppSelector } from 'app/store/storeHooks'; import dateFormat, { masks } from 'dateformat'; +import { selectWorkflowId } from 'features/nodes/store/workflowSlice'; import { useWorkflowLibraryModalContext } from 'features/workflowLibrary/context/useWorkflowLibraryModalContext'; import { useDeleteLibraryWorkflow } from 'features/workflowLibrary/hooks/useDeleteLibraryWorkflow'; import { useGetAndLoadLibraryWorkflow } from 'features/workflowLibrary/hooks/useGetAndLoadLibraryWorkflow'; @@ -14,9 +16,9 @@ type Props = { const WorkflowLibraryListItem = ({ workflowDTO }: Props) => { const { t } = useTranslation(); - const workflowId = useAppSelector((s) => s.workflow.id); + const workflowId = useAppSelector(selectWorkflowId); const { onClose } = useWorkflowLibraryModalContext(); - const { deleteWorkflow, deleteWorkflowResult } = useDeleteLibraryWorkflow({}); + const { deleteWorkflow, deleteWorkflowResult } = useDeleteLibraryWorkflow(EMPTY_OBJECT); const { getAndLoadWorkflow, getAndLoadWorkflowResult } = useGetAndLoadLibraryWorkflow({ onSuccess: onClose }); const handleDeleteWorkflow = useCallback(() => { diff --git a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx index 0a973cc92c..c1335b1dc9 100644 --- a/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx +++ b/invokeai/frontend/web/src/features/workflowLibrary/components/WorkflowLibraryMenu/SaveWorkflowMenuItem.tsx @@ -1,6 +1,7 @@ import { MenuItem } from '@invoke-ai/ui-library'; import { useAppSelector } from 'app/store/storeHooks'; import { $builtWorkflow } from 'features/nodes/hooks/useWorkflowWatcher'; +import { selectWorkflowIsTouched } from 'features/nodes/store/workflowSlice'; import { useSaveWorkflowAsDialog } from 'features/workflowLibrary/components/SaveWorkflowAsDialog/useSaveWorkflowAsDialog'; import { isWorkflowWithID, useSaveLibraryWorkflow } from 'features/workflowLibrary/hooks/useSaveWorkflow'; import { memo, useCallback } from 'react'; @@ -11,7 +12,7 @@ const SaveWorkflowMenuItem = () => { const { t } = useTranslation(); const { saveWorkflow } = useSaveLibraryWorkflow(); const { onOpen } = useSaveWorkflowAsDialog(); - const isTouched = useAppSelector((s) => s.workflow.isTouched); + const isTouched = useAppSelector(selectWorkflowIsTouched); const handleClickSave = useCallback(() => { const builtWorkflow = $builtWorkflow.get(); diff --git a/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts b/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts index 3f303fbf10..df178225ac 100644 --- a/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts +++ b/invokeai/frontend/web/src/services/api/hooks/useDebouncedImageWorkflow.ts @@ -1,11 +1,12 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectWorkflowFetchDebounce } from 'features/system/store/configSlice'; import { useGetImageWorkflowQuery } from 'services/api/endpoints/images'; import type { ImageDTO } from 'services/api/types'; import { useDebounce } from 'use-debounce'; export const useDebouncedImageWorkflow = (imageDTO?: ImageDTO | null) => { - const workflowFetchDebounce = useAppSelector((s) => s.config.workflowFetchDebounce ?? 300); + const workflowFetchDebounce = useAppSelector(selectWorkflowFetchDebounce); const [debouncedImageName] = useDebounce(imageDTO?.has_workflow ? imageDTO.image_name : null, workflowFetchDebounce); diff --git a/invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts b/invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts index 83f8728fd2..81e36c95a7 100644 --- a/invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts +++ b/invokeai/frontend/web/src/services/api/hooks/useDebouncedMetadata.ts @@ -1,10 +1,11 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectMetadataFetchDebounce } from 'features/system/store/configSlice'; import { useGetImageMetadataQuery } from 'services/api/endpoints/images'; import { useDebounce } from 'use-debounce'; export const useDebouncedMetadata = (imageName?: string | null) => { - const metadataFetchDebounce = useAppSelector((s) => s.config.metadataFetchDebounce ?? 300); + const metadataFetchDebounce = useAppSelector(selectMetadataFetchDebounce); const [debouncedImageName] = useDebounce(imageName, metadataFetchDebounce); diff --git a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts index d4dbdd37fd..6f35a5fe91 100644 --- a/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts +++ b/invokeai/frontend/web/src/services/api/hooks/useSelectedModelConfig.ts @@ -1,9 +1,10 @@ import { skipToken } from '@reduxjs/toolkit/query'; import { useAppSelector } from 'app/store/storeHooks'; +import { selectModelKey } from 'features/controlLayers/store/paramsSlice'; import { useGetModelConfigQuery } from 'services/api/endpoints/models'; export const useSelectedModelConfig = () => { - const key = useAppSelector((s) => s.params.model?.key); + const key = useAppSelector(selectModelKey); const { currentData: modelConfig } = useGetModelConfigQuery(key ?? skipToken); return modelConfig;