diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 92af3b2ec8..c96be420bb 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -3,55 +3,67 @@ import { createSelector, createSlice } from '@reduxjs/toolkit'; import type { PersistConfig, RootState } from 'app/store/store'; import { canvasReset } from 'features/controlLayers/store/actions'; import { canvasSessionReset, generateSessionReset } from 'features/controlLayers/store/canvasStagingAreaSlice'; -import type { Dimensions } from 'features/controlLayers/store/types'; import { workflowLoaded } from 'features/nodes/store/nodesSlice'; import { atom } from 'nanostores'; -import type { CanvasRightPanelTabName, TabName, UIState } from './uiTypes'; - -const initialUIState: UIState = { - _version: 3, - activeTab: 'canvas', - activeTabCanvasRightPanel: 'gallery', - shouldShowImageDetails: false, - shouldShowProgressInViewer: true, - accordions: {}, - expanders: {}, - textAreaSizes: {}, - shouldShowNotificationV2: true, -}; +import type { TabName, UIState } from './uiTypes'; +import { getInitialUIState } from './uiTypes'; export const uiSlice = createSlice({ name: 'ui', - initialState: initialUIState, + initialState: getInitialUIState(), reducers: { - setActiveTab: (state, action: PayloadAction) => { + setActiveTab: (state, action: PayloadAction) => { state.activeTab = action.payload; }, - activeTabCanvasRightPanelChanged: (state, action: PayloadAction) => { + activeTabCanvasRightPanelChanged: (state, action: PayloadAction) => { state.activeTabCanvasRightPanel = action.payload; }, - setShouldShowImageDetails: (state, action: PayloadAction) => { + setShouldShowImageDetails: (state, action: PayloadAction) => { state.shouldShowImageDetails = action.payload; }, - setShouldShowProgressInViewer: (state, action: PayloadAction) => { + setShouldShowProgressInViewer: (state, action: PayloadAction) => { state.shouldShowProgressInViewer = action.payload; }, - accordionStateChanged: (state, action: PayloadAction<{ id: string; isOpen: boolean }>) => { + accordionStateChanged: ( + state, + action: PayloadAction<{ + id: keyof UIState['accordions']; + isOpen: UIState['accordions'][keyof UIState['accordions']]; + }> + ) => { const { id, isOpen } = action.payload; state.accordions[id] = isOpen; }, - expanderStateChanged: (state, action: PayloadAction<{ id: string; isOpen: boolean }>) => { + expanderStateChanged: ( + state, + action: PayloadAction<{ + id: keyof UIState['expanders']; + isOpen: UIState['expanders'][keyof UIState['expanders']]; + }> + ) => { const { id, isOpen } = action.payload; state.expanders[id] = isOpen; }, - textAreaSizesStateChanged: (state, action: PayloadAction<{ id: string; size: Partial }>) => { + textAreaSizesStateChanged: ( + state, + action: PayloadAction<{ + id: keyof UIState['textAreaSizes']; + size: UIState['textAreaSizes'][keyof UIState['textAreaSizes']]; + }> + ) => { const { id, size } = action.payload; state.textAreaSizes[id] = size; }, - shouldShowNotificationChanged: (state, action: PayloadAction) => { + shouldShowNotificationChanged: (state, action: PayloadAction) => { state.shouldShowNotificationV2 = action.payload; }, + showGenerateTabSplashScreenChanged: (state, action: PayloadAction) => { + state.showGenerateTabSplashScreen = action.payload; + }, + showCanvasTabSplashScreenChanged: (state, action: PayloadAction) => { + state.showCanvasTabSplashScreen = action.payload; + }, }, extraReducers(builder) { builder.addCase(workflowLoaded, (state) => { @@ -81,6 +93,8 @@ export const { expanderStateChanged, shouldShowNotificationChanged, textAreaSizesStateChanged, + showGenerateTabSplashScreenChanged, + showCanvasTabSplashScreenChanged, } = uiSlice.actions; export const selectUiSlice = (state: RootState) => state.ui; @@ -103,7 +117,7 @@ const migrateUIState = (state: any): any => { export const uiPersistConfig: PersistConfig = { name: uiSlice.name, - initialState: initialUIState, + initialState: getInitialUIState(), migrate: migrateUIState, persistDenylist: ['shouldShowImageDetails'], }; diff --git a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts index 3702c58ec5..6c0ab9023a 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiTypes.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiTypes.ts @@ -1,43 +1,30 @@ -import type { Dimensions } from 'features/controlLayers/store/types'; +import { deepClone } from 'common/util/deepClone'; +import { z } from 'zod'; -export type TabName = 'generate' | 'canvas' | 'upscaling' | 'workflows' | 'models' | 'queue'; -export type CanvasRightPanelTabName = 'layers' | 'gallery'; +const zTabName = z.enum(['generate', 'canvas', 'upscaling', 'workflows', 'models', 'queue']); +export type TabName = z.infer; +const zCanvasRightPanelTabName = z.enum(['layers', 'gallery']); +export type CanvasRightPanelTabName = z.infer; -export interface UIState { - /** - * Slice schema version. - */ - _version: 3; - /** - * The currently active tab. - */ - activeTab: TabName; - /** - * The currently active right panel canvas tab - */ - activeTabCanvasRightPanel: CanvasRightPanelTabName; - /** - * Whether or not to show image details, e.g. metadata, workflow, etc. - */ - shouldShowImageDetails: boolean; - /** - * Whether or not to show progress in the viewer. - */ - shouldShowProgressInViewer: boolean; - /** - * The state of accordions. The key is the id of the accordion, and the value is a boolean representing the open state. - */ - accordions: Record; - /** - * The state of expanders. The key is the id of the expander, and the value is a boolean representing the open state. - */ - expanders: Record; - /** - * The size of textareas. The key is the id of the text area, and the value is an object representing its width and/or height. - */ - textAreaSizes: Record>; - /** - * Whether or not to show the user the open notification. Bump version to reset users who may have closed previous version. - */ - shouldShowNotificationV2: boolean; -} +const zPartialDimensions = z.object({ + width: z.number().optional(), + height: z.number().optional(), +}); +export type PartialDimensions = z.infer; + +export const zUIState = z.object({ + _version: z.literal(3).default(3), + activeTab: zTabName.default('canvas'), + activeTabCanvasRightPanel: zCanvasRightPanelTabName.default('gallery'), + shouldShowImageDetails: z.boolean().default(false), + shouldShowProgressInViewer: z.boolean().default(true), + accordions: z.record(z.string(), z.boolean()).default(() => ({})), + expanders: z.record(z.string(), z.boolean()).default(() => ({})), + textAreaSizes: z.record(z.string(), zPartialDimensions).default({}), + shouldShowNotificationV2: z.boolean().default(true), + showGenerateTabSplashScreen: z.boolean().default(true), + showCanvasTabSplashScreen: z.boolean().default(true), +}); +const INITIAL_STATE = zUIState.parse({}); +export type UIState = z.infer; +export const getInitialUIState = (): UIState => deepClone(INITIAL_STATE);