From 09f166577e171963c17ad5ebaf08e02d68f08d59 Mon Sep 17 00:00:00 2001 From: psychedelicious <4822129+psychedelicious@users.noreply.github.com> Date: Sat, 6 May 2023 00:28:00 +1000 Subject: [PATCH] feat(ui): migrate to redux-remember --- invokeai/frontend/web/package.json | 1 + .../web/src/app/components/InvokeAIUI.tsx | 14 +- .../frontend/web/src/app/store/actions.ts | 4 + .../frontend/web/src/app/store/constants.ts | 8 + .../enhancers/reduxRemember/serialize.ts | 36 ++++ .../enhancers/reduxRemember/unserialize.ts | 49 +++++ .../middleware/devtools/actionSanitizer.ts | 29 +++ .../middleware/devtools/actionsDenylist.ts | 11 ++ .../middleware/devtools/stateSanitizer.ts | 3 + .../middleware/listenerMiddleware/index.ts | 11 +- .../{userInvoked.ts => userInvokedCanvas.ts} | 45 +---- .../listeners/userInvokedCreate.ts | 24 +++ .../listeners/userInvokedNodes.ts | 24 +++ .../frontend/web/src/app/store/persistor.ts | 4 - invokeai/frontend/web/src/app/store/store.ts | 169 +++++------------- .../canvas/store/canvasPersistDenylist.ts | 6 + .../src/features/canvas/store/canvasSlice.ts | 2 +- .../gallery/store/galleryPersistDenylist.ts | 5 + .../gallery/store/gallerySelectors.ts | 7 +- .../features/gallery/store/gallerySlice.ts | 4 +- .../gallery/store/resultsPersistDenylist.ts | 2 + .../gallery/store/uploadsPersistDenylist.ts | 1 + .../features/gallery/store/uploadsSlice.ts | 2 +- .../lightbox/store/lightboxPersistDenylist.ts | 3 + .../features/lightbox/store/lightboxSlice.ts | 2 +- .../components/panels/TopCenterPanel.tsx | 2 +- .../nodes/store/nodesPersistDenylist.ts | 4 + .../ProcessButtons/InvokeButton.tsx | 2 +- .../components/PromptInput/PromptInput.tsx | 2 +- .../store/generationPersistDenylist.ts | 1 + .../parameters/store/generationSlice.ts | 8 +- .../store/postprocessingPersistDenylist.ts | 1 + .../parameters/store/postprocessingSlice.ts | 6 +- .../system/components/ModelSelect.tsx | 14 +- .../SettingsModal/SettingsModal.tsx | 19 +- .../src/features/system/store/configSlice.ts | 2 +- .../src/features/system/store/modelSlice.ts | 36 ++-- .../system/store/modelsPersistDenylist.ts | 1 + .../system/store/systemPersistDenylist.ts | 19 ++ .../src/features/system/store/systemSlice.ts | 8 +- .../web/src/features/ui/store/hotkeysSlice.ts | 6 +- .../features/ui/store/uiPersistDenylist.ts | 3 + .../web/src/features/ui/store/uiSlice.ts | 6 +- invokeai/frontend/web/src/i18n.ts | 9 +- .../web/src/services/events/middleware.ts | 6 +- .../web/src/services/thunks/session.ts | 66 ------- invokeai/frontend/web/yarn.lock | 5 + 47 files changed, 379 insertions(+), 313 deletions(-) create mode 100644 invokeai/frontend/web/src/app/store/constants.ts create mode 100644 invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts create mode 100644 invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts create mode 100644 invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts create mode 100644 invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts create mode 100644 invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts rename invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/{userInvoked.ts => userInvokedCanvas.ts} (71%) create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCreate.ts create mode 100644 invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts delete mode 100644 invokeai/frontend/web/src/app/store/persistor.ts diff --git a/invokeai/frontend/web/package.json b/invokeai/frontend/web/package.json index da5e58c9c9..5d704f67d2 100644 --- a/invokeai/frontend/web/package.json +++ b/invokeai/frontend/web/package.json @@ -99,6 +99,7 @@ "redux-deep-persist": "^1.0.7", "redux-dynamic-middlewares": "^2.2.0", "redux-persist": "^6.0.0", + "redux-remember": "^3.2.1", "roarr": "^7.15.0", "serialize-error": "^11.0.0", "socket.io-client": "^4.6.0", diff --git a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx index 97a8be6fc1..d9d99de6cf 100644 --- a/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx +++ b/invokeai/frontend/web/src/app/components/InvokeAIUI.tsx @@ -1,8 +1,6 @@ import React, { lazy, memo, PropsWithChildren, useEffect } from 'react'; import { Provider } from 'react-redux'; -import { PersistGate } from 'redux-persist/integration/react'; import { store } from 'app/store/store'; -import { persistor } from '../store/persistor'; import { OpenAPI } from 'services/api'; import '@fontsource/inter/100.css'; import '@fontsource/inter/200.css'; @@ -57,13 +55,11 @@ const InvokeAIUI = ({ apiUrl, token, config, children }: Props) => { return ( - } persistor={persistor}> - }> - - {children} - - - + }> + + {children} + + ); diff --git a/invokeai/frontend/web/src/app/store/actions.ts b/invokeai/frontend/web/src/app/store/actions.ts index e69de29bb2..e07920ce0c 100644 --- a/invokeai/frontend/web/src/app/store/actions.ts +++ b/invokeai/frontend/web/src/app/store/actions.ts @@ -0,0 +1,4 @@ +import { createAction } from '@reduxjs/toolkit'; +import { InvokeTabName } from 'features/ui/store/tabMap'; + +export const userInvoked = createAction('app/userInvoked'); diff --git a/invokeai/frontend/web/src/app/store/constants.ts b/invokeai/frontend/web/src/app/store/constants.ts new file mode 100644 index 0000000000..6d48762bef --- /dev/null +++ b/invokeai/frontend/web/src/app/store/constants.ts @@ -0,0 +1,8 @@ +export const LOCALSTORAGE_KEYS = [ + 'chakra-ui-color-mode', + 'i18nextLng', + 'ROARR_FILTER', + 'ROARR_LOG', +]; + +export const LOCALSTORAGE_PREFIX = '@@invokeai-'; diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts new file mode 100644 index 0000000000..52995e0da3 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/serialize.ts @@ -0,0 +1,36 @@ +import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist'; +import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist'; +import { resultsPersistDenylist } from 'features/gallery/store/resultsPersistDenylist'; +import { uploadsPersistDenylist } from 'features/gallery/store/uploadsPersistDenylist'; +import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist'; +import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist'; +import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist'; +import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; +import { modelsPersistDenylist } from 'features/system/store/modelsPersistDenylist'; +import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist'; +import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist'; +import { omit } from 'lodash-es'; +import { SerializeFunction } from 'redux-remember'; + +const serializationDenylist: { + [key: string]: string[]; +} = { + canvas: canvasPersistDenylist, + gallery: galleryPersistDenylist, + generation: generationPersistDenylist, + lightbox: lightboxPersistDenylist, + models: modelsPersistDenylist, + nodes: nodesPersistDenylist, + postprocessing: postprocessingPersistDenylist, + results: resultsPersistDenylist, + system: systemPersistDenylist, + // config: configPersistDenyList, + ui: uiPersistDenylist, + uploads: uploadsPersistDenylist, + // hotkeys: hotkeysPersistDenylist, +}; + +export const serialize: SerializeFunction = (data, key) => { + const result = omit(data, serializationDenylist[key]); + return JSON.stringify(result); +}; diff --git a/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts new file mode 100644 index 0000000000..d775e06187 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/enhancers/reduxRemember/unserialize.ts @@ -0,0 +1,49 @@ +import { canvasPersistDenylist } from 'features/canvas/store/canvasPersistDenylist'; +import { initialCanvasState } from 'features/canvas/store/canvasSlice'; +import { galleryPersistDenylist } from 'features/gallery/store/galleryPersistDenylist'; +import { initialGalleryState } from 'features/gallery/store/gallerySlice'; +import { resultsPersistDenylist } from 'features/gallery/store/resultsPersistDenylist'; +import { initialResultsState } from 'features/gallery/store/resultsSlice'; +import { uploadsPersistDenylist } from 'features/gallery/store/uploadsPersistDenylist'; +import { initialUploadsState } from 'features/gallery/store/uploadsSlice'; +import { lightboxPersistDenylist } from 'features/lightbox/store/lightboxPersistDenylist'; +import { initialLightboxState } from 'features/lightbox/store/lightboxSlice'; +import { nodesPersistDenylist } from 'features/nodes/store/nodesPersistDenylist'; +import { initialNodesState } from 'features/nodes/store/nodesSlice'; +import { generationPersistDenylist } from 'features/parameters/store/generationPersistDenylist'; +import { initialGenerationState } from 'features/parameters/store/generationSlice'; +import { postprocessingPersistDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; +import { initialPostprocessingState } from 'features/parameters/store/postprocessingSlice'; +import { initialConfigState } from 'features/system/store/configSlice'; +import { initialModelsState } from 'features/system/store/modelSlice'; +import { modelsPersistDenylist } from 'features/system/store/modelsPersistDenylist'; +import { systemPersistDenylist } from 'features/system/store/systemPersistDenylist'; +import { initialSystemState } from 'features/system/store/systemSlice'; +import { initialHotkeysState } from 'features/ui/store/hotkeysSlice'; +import { uiPersistDenylist } from 'features/ui/store/uiPersistDenylist'; +import { initialUIState } from 'features/ui/store/uiSlice'; +import { defaultsDeep, merge, omit } from 'lodash-es'; +import { UnserializeFunction } from 'redux-remember'; + +const initialStates: { + [key: string]: any; +} = { + canvas: initialCanvasState, + gallery: initialGalleryState, + generation: initialGenerationState, + lightbox: initialLightboxState, + models: initialModelsState, + nodes: initialNodesState, + postprocessing: initialPostprocessingState, + results: initialResultsState, + system: initialSystemState, + config: initialConfigState, + ui: initialUIState, + uploads: initialUploadsState, + hotkeys: initialHotkeysState, +}; + +export const unserialize: UnserializeFunction = (data, key) => { + const result = defaultsDeep(JSON.parse(data), initialStates[key]); + return result; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts new file mode 100644 index 0000000000..c9d8e45886 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionSanitizer.ts @@ -0,0 +1,29 @@ +import { AnyAction } from '@reduxjs/toolkit'; +import { isAnyGraphBuilt } from 'features/nodes/store/actions'; +import { forEach } from 'lodash-es'; +import { Graph } from 'services/api'; + +export const actionSanitizer = (action: A): A => { + if (isAnyGraphBuilt(action)) { + if (action.payload.nodes) { + const sanitizedNodes: Graph['nodes'] = {}; + + // Sanitize nodes as needed + forEach(action.payload.nodes, (node, key) => { + if (node.type === 'dataURL_image') { + const { dataURL, ...rest } = node; + sanitizedNodes[key] = { ...rest, dataURL: '' }; + } else { + sanitizedNodes[key] = { ...node }; + } + }); + + return { + ...action, + payload: { ...action.payload, nodes: sanitizedNodes }, + }; + } + } + + return action; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts new file mode 100644 index 0000000000..743537d7ea --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/actionsDenylist.ts @@ -0,0 +1,11 @@ +export const actionsDenylist = [ + 'canvas/setCursorPosition', + 'canvas/setStageCoordinates', + 'canvas/setStageScale', + 'canvas/setIsDrawing', + 'canvas/setBoundingBoxCoordinates', + 'canvas/setBoundingBoxDimensions', + 'canvas/setIsDrawing', + 'canvas/addPointToCurrentLine', + 'socket/generatorProgress', +]; diff --git a/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts b/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts new file mode 100644 index 0000000000..312b4db189 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/devtools/stateSanitizer.ts @@ -0,0 +1,3 @@ +export const stateSanitizer = (state: S): S => { + return state; +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts index 5869038d6a..6e66e19780 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/index.ts @@ -11,12 +11,9 @@ import { addInitialImageSelectedListener } from './listeners/initialImageSelecte import { addImageResultReceivedListener } from './listeners/invocationComplete'; import { addImageUploadedListener } from './listeners/imageUploaded'; import { addRequestedImageDeletionListener } from './listeners/imageDeleted'; -import { - addUserInvokedCanvasListener, - addUserInvokedCreateListener, - addUserInvokedNodesListener, -} from './listeners/userInvoked'; -import { addCanvasGraphBuiltListener } from './listeners/canvasGraphBuilt'; +import { addUserInvokedCanvasListener } from './listeners/userInvokedCanvas'; +import { addUserInvokedCreateListener } from './listeners/userInvokedCreate'; +import { addUserInvokedNodesListener } from './listeners/userInvokedNodes'; export const listenerMiddleware = createListenerMiddleware(); @@ -40,7 +37,7 @@ addImageUploadedListener(); addInitialImageSelectedListener(); addImageResultReceivedListener(); addRequestedImageDeletionListener(); + addUserInvokedCanvasListener(); addUserInvokedCreateListener(); addUserInvokedNodesListener(); -// addCanvasGraphBuiltListener(); diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvoked.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts similarity index 71% rename from invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvoked.ts rename to invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts index 63da80440e..2490a358fe 100644 --- a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvoked.ts +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCanvas.ts @@ -1,16 +1,8 @@ -import { createAction } from '@reduxjs/toolkit'; import { startAppListening } from '..'; -import { InvokeTabName } from 'features/ui/store/tabMap'; -import { buildLinearGraph } from 'features/nodes/util/buildLinearGraph'; import { sessionCreated, sessionInvoked } from 'services/thunks/session'; import { buildCanvasGraphAndBlobs } from 'features/nodes/util/buildCanvasGraph'; -import { buildNodesGraph } from 'features/nodes/util/buildNodesGraph'; import { log } from 'app/logging/useLogger'; -import { - canvasGraphBuilt, - createGraphBuilt, - nodesGraphBuilt, -} from 'features/nodes/store/actions'; +import { canvasGraphBuilt } from 'features/nodes/store/actions'; import { imageUploaded } from 'services/thunks/image'; import { v4 as uuidv4 } from 'uuid'; import { Graph } from 'services/api'; @@ -18,27 +10,10 @@ import { canvasSessionIdChanged, stagingAreaInitialized, } from 'features/canvas/store/canvasSlice'; +import { userInvoked } from 'app/store/actions'; const moduleLog = log.child({ namespace: 'invoke' }); -export const userInvoked = createAction('app/userInvoked'); - -export const addUserInvokedCreateListener = () => { - startAppListening({ - predicate: (action): action is ReturnType => - userInvoked.match(action) && action.payload === 'generate', - effect: (action, { getState, dispatch }) => { - const state = getState(); - - const graph = buildLinearGraph(state); - dispatch(createGraphBuilt(graph)); - moduleLog({ data: graph }, 'Create graph built'); - - dispatch(sessionCreated({ graph })); - }, - }); -}; - export const addUserInvokedCanvasListener = () => { startAppListening({ predicate: (action): action is ReturnType => @@ -149,19 +124,3 @@ export const addUserInvokedCanvasListener = () => { }, }); }; - -export const addUserInvokedNodesListener = () => { - startAppListening({ - predicate: (action): action is ReturnType => - userInvoked.match(action) && action.payload === 'nodes', - effect: (action, { getState, dispatch }) => { - const state = getState(); - - const graph = buildNodesGraph(state); - dispatch(nodesGraphBuilt(graph)); - moduleLog({ data: graph }, 'Nodes graph built'); - - dispatch(sessionCreated({ graph })); - }, - }); -}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCreate.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCreate.ts new file mode 100644 index 0000000000..d24385ea97 --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedCreate.ts @@ -0,0 +1,24 @@ +import { startAppListening } from '..'; +import { buildLinearGraph } from 'features/nodes/util/buildLinearGraph'; +import { sessionCreated } from 'services/thunks/session'; +import { log } from 'app/logging/useLogger'; +import { createGraphBuilt } from 'features/nodes/store/actions'; +import { userInvoked } from 'app/store/actions'; + +const moduleLog = log.child({ namespace: 'invoke' }); + +export const addUserInvokedCreateListener = () => { + startAppListening({ + predicate: (action): action is ReturnType => + userInvoked.match(action) && action.payload === 'generate', + effect: (action, { getState, dispatch }) => { + const state = getState(); + + const graph = buildLinearGraph(state); + dispatch(createGraphBuilt(graph)); + moduleLog({ data: graph }, 'Create graph built'); + + dispatch(sessionCreated({ graph })); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts new file mode 100644 index 0000000000..baf7bc5baf --- /dev/null +++ b/invokeai/frontend/web/src/app/store/middleware/listenerMiddleware/listeners/userInvokedNodes.ts @@ -0,0 +1,24 @@ +import { startAppListening } from '..'; +import { sessionCreated } from 'services/thunks/session'; +import { buildNodesGraph } from 'features/nodes/util/buildNodesGraph'; +import { log } from 'app/logging/useLogger'; +import { nodesGraphBuilt } from 'features/nodes/store/actions'; +import { userInvoked } from 'app/store/actions'; + +const moduleLog = log.child({ namespace: 'invoke' }); + +export const addUserInvokedNodesListener = () => { + startAppListening({ + predicate: (action): action is ReturnType => + userInvoked.match(action) && action.payload === 'nodes', + effect: (action, { getState, dispatch }) => { + const state = getState(); + + const graph = buildNodesGraph(state); + dispatch(nodesGraphBuilt(graph)); + moduleLog({ data: graph }, 'Nodes graph built'); + + dispatch(sessionCreated({ graph })); + }, + }); +}; diff --git a/invokeai/frontend/web/src/app/store/persistor.ts b/invokeai/frontend/web/src/app/store/persistor.ts deleted file mode 100644 index 85dc934943..0000000000 --- a/invokeai/frontend/web/src/app/store/persistor.ts +++ /dev/null @@ -1,4 +0,0 @@ -import { store } from 'app/store/store'; -import { persistStore } from 'redux-persist'; - -export const persistor = persistStore(store); diff --git a/invokeai/frontend/web/src/app/store/store.ts b/invokeai/frontend/web/src/app/store/store.ts index 15eb045405..777f563cae 100644 --- a/invokeai/frontend/web/src/app/store/store.ts +++ b/invokeai/frontend/web/src/app/store/store.ts @@ -1,16 +1,13 @@ import { - Action, AnyAction, + Store, ThunkDispatch, combineReducers, configureStore, - isAnyOf, } from '@reduxjs/toolkit'; -import { persistReducer } from 'redux-persist'; -import storage from 'redux-persist/lib/storage'; // defaults to localStorage for web +import { rememberReducer, rememberEnhancer } from 'redux-remember'; import dynamicMiddlewares from 'redux-dynamic-middlewares'; -import { getPersistConfig } from 'redux-deep-persist'; import canvasReducer from 'features/canvas/store/canvasSlice'; import galleryReducer from 'features/gallery/store/gallerySlice'; @@ -26,35 +23,17 @@ import hotkeysReducer from 'features/ui/store/hotkeysSlice'; import modelsReducer from 'features/system/store/modelSlice'; import nodesReducer from 'features/nodes/store/nodesSlice'; -import { canvasDenylist } from 'features/canvas/store/canvasPersistDenylist'; -import { galleryDenylist } from 'features/gallery/store/galleryPersistDenylist'; -import { generationDenylist } from 'features/parameters/store/generationPersistDenylist'; -import { lightboxDenylist } from 'features/lightbox/store/lightboxPersistDenylist'; -import { modelsDenylist } from 'features/system/store/modelsPersistDenylist'; -import { nodesDenylist } from 'features/nodes/store/nodesPersistDenylist'; -import { postprocessingDenylist } from 'features/parameters/store/postprocessingPersistDenylist'; -import { systemDenylist } from 'features/system/store/systemPersistDenylist'; -import { uiDenylist } from 'features/ui/store/uiPersistDenylist'; import { listenerMiddleware } from './middleware/listenerMiddleware'; -import { isAnyGraphBuilt } from 'features/nodes/store/actions'; -import { forEach } from 'lodash-es'; -import { Graph } from 'services/api'; -/** - * redux-persist provides an easy and reliable way to persist state across reloads. - * - * While we definitely want generation parameters to be persisted, there are a number - * of things we do *not* want to be persisted across reloads: - * - Gallery/selected image (user may add/delete images from disk between page loads) - * - Connection/processing status - * - Availability of external libraries like ESRGAN/GFPGAN - * - * These can be denylisted in redux-persist. - * - * The necesssary nested persistors with denylists are configured below. - */ +import { actionSanitizer } from './middleware/devtools/actionSanitizer'; +import { stateSanitizer } from './middleware/devtools/stateSanitizer'; +import { actionsDenylist } from './middleware/devtools/actionsDenylist'; -const rootReducer = combineReducers({ +import { serialize } from './enhancers/reduxRemember/serialize'; +import { unserialize } from './enhancers/reduxRemember/unserialize'; +import { LOCALSTORAGE_PREFIX } from './constants'; + +const allReducers = { canvas: canvasReducer, gallery: galleryReducer, generation: generationReducer, @@ -68,65 +47,38 @@ const rootReducer = combineReducers({ ui: uiReducer, uploads: uploadsReducer, hotkeys: hotkeysReducer, -}); +}; -const rootPersistConfig = getPersistConfig({ - key: 'root', - storage, - rootReducer, - blacklist: [ - ...canvasDenylist, - ...galleryDenylist, - ...generationDenylist, - ...lightboxDenylist, - ...modelsDenylist, - ...nodesDenylist, - ...postprocessingDenylist, - // ...resultsDenylist, - 'results', - ...systemDenylist, - ...uiDenylist, - // ...uploadsDenylist, - 'uploads', - 'hotkeys', - 'config', +const rootReducer = combineReducers(allReducers); + +const rememberedRootReducer = rememberReducer(rootReducer); + +const rememberedKeys: (keyof typeof allReducers)[] = [ + 'canvas', + 'gallery', + 'generation', + 'lightbox', + // 'models', + 'nodes', + 'postprocessing', + 'system', + 'ui', + // 'hotkeys', + // 'results', + // 'uploads', + // 'config', +]; + +export const store: Store = configureStore({ + reducer: rememberedRootReducer, + enhancers: [ + rememberEnhancer(window.localStorage, rememberedKeys, { + persistDebounce: 300, + serialize, + unserialize, + prefix: LOCALSTORAGE_PREFIX, + }), ], -}); - -const persistedReducer = persistReducer(rootPersistConfig, rootReducer); - -// TODO: rip the old middleware out when nodes is complete -// export function buildMiddleware() { -// if (import.meta.env.MODE === 'nodes' || import.meta.env.MODE === 'package') { -// return socketMiddleware(); -// } else { -// return socketioMiddleware(); -// } -// } - -// const actionSanitizer = (action: AnyAction): AnyAction => { -// if (isAnyGraphBuilt(action)) { -// if (action.payload.nodes) { -// const sanitizedNodes: Graph['nodes'] = {}; -// forEach(action.payload.nodes, (node, key) => { -// if (node.type === 'dataURL_image') { -// const { dataURL, ...rest } = node; -// sanitizedNodes[key] = { ...rest, dataURL: '<>' }; -// } -// }); -// const sanitizedAction: AnyAction = { -// ...action, -// payload: { ...action.payload, nodes: sanitizedNodes }, -// }; -// return sanitizedAction; -// } -// } - -// return action; -// }; - -export const store = configureStore({ - reducer: persistedReducer, middleware: (getDefaultMiddleware) => getDefaultMiddleware({ immutableCheck: false, @@ -135,43 +87,10 @@ export const store = configureStore({ .concat(dynamicMiddlewares) .prepend(listenerMiddleware.middleware), devTools: { - // Uncommenting these very rapidly called actions makes the redux dev tools output much more readable - actionsDenylist: [ - 'canvas/setCursorPosition', - 'canvas/setStageCoordinates', - 'canvas/setStageScale', - 'canvas/setIsDrawing', - 'canvas/setBoundingBoxCoordinates', - 'canvas/setBoundingBoxDimensions', - 'canvas/setIsDrawing', - 'canvas/addPointToCurrentLine', - 'socket/generatorProgress', - ], - actionSanitizer: (action) => { - if (isAnyGraphBuilt(action)) { - if (action.payload.nodes) { - const sanitizedNodes: Graph['nodes'] = {}; - - forEach(action.payload.nodes, (node, key) => { - if (node.type === 'dataURL_image') { - const { dataURL, ...rest } = node; - sanitizedNodes[key] = { ...rest, dataURL: '<>' }; - } else { - sanitizedNodes[key] = { ...node }; - } - }); - - return { - ...action, - payload: { ...action.payload, nodes: sanitizedNodes }, - }; - } - } - - return action; - }, - // stateSanitizer: (state) => - // state.data ? { ...state, data: '<>' } : state, + actionsDenylist, + actionSanitizer, + stateSanitizer, + trace: true, }, }); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts b/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts index abaefab8b0..1f44b43021 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasPersistDenylist.ts @@ -9,6 +9,12 @@ const itemsToDenylist: (keyof CanvasState)[] = [ 'doesCanvasNeedScaling', ]; +export const canvasPersistDenylist: (keyof CanvasState)[] = [ + 'cursorPosition', + 'isCanvasInitialized', + 'doesCanvasNeedScaling', +]; + export const canvasDenylist = itemsToDenylist.map( (denylistItem) => `canvas.${denylistItem}` ); diff --git a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts index 438f4df9e1..67adcc1769 100644 --- a/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts +++ b/invokeai/frontend/web/src/features/canvas/store/canvasSlice.ts @@ -40,7 +40,7 @@ export const initialLayerState: CanvasLayerState = { }, }; -const initialCanvasState: CanvasState = { +export const initialCanvasState: CanvasState = { boundingBoxCoordinates: { x: 0, y: 0 }, boundingBoxDimensions: { width: 512, height: 512 }, boundingBoxPreviewFill: { r: 0, g: 0, b: 0, a: 0.5 }, diff --git a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts index 8c91a79dfb..dcec4fa373 100644 --- a/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/galleryPersistDenylist.ts @@ -8,6 +8,11 @@ const itemsToDenylist: (keyof GalleryState)[] = [ 'shouldAutoSwitchToNewImages', ]; +export const galleryPersistDenylist: (keyof GalleryState)[] = [ + 'currentCategory', + 'shouldAutoSwitchToNewImages', +]; + export const galleryDenylist = itemsToDenylist.map( (denylistItem) => `gallery.${denylistItem}` ); diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts index 0fc8d300e9..082e644b43 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySelectors.ts @@ -44,6 +44,11 @@ export const imageGallerySelector = createSelector( const { isLightboxOpen } = lightbox; + const images = + currentCategory === 'results' + ? selectResultsEntities(state) + : selectUploadsAll(state); + return { shouldPinGallery, galleryImageMinimumWidth, @@ -53,7 +58,7 @@ export const imageGallerySelector = createSelector( : `repeat(auto-fill, minmax(${galleryImageMinimumWidth}px, auto))`, shouldAutoSwitchToNewImages, currentCategory, - images: state[currentCategory].entities, + images, galleryWidth, shouldEnableResize: isLightboxOpen || diff --git a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts index 630bd0d6b3..2326295451 100644 --- a/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/gallerySlice.ts @@ -17,7 +17,7 @@ export interface GalleryState { currentCategory: 'results' | 'uploads'; } -const initialState: GalleryState = { +export const initialGalleryState: GalleryState = { galleryImageMinimumWidth: 64, galleryImageObjectFit: 'cover', shouldAutoSwitchToNewImages: true, @@ -28,7 +28,7 @@ const initialState: GalleryState = { export const gallerySlice = createSlice({ name: 'gallery', - initialState, + initialState: initialGalleryState, reducers: { imageSelected: (state, action: PayloadAction) => { state.selectedImage = action.payload; diff --git a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts index ef21f4b7b2..4b8ccac6a7 100644 --- a/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/resultsPersistDenylist.ts @@ -7,6 +7,8 @@ import { ResultsState } from './resultsSlice'; */ const itemsToDenylist: (keyof ResultsState)[] = []; +export const resultsPersistDenylist: (keyof ResultsState)[] = []; + export const resultsDenylist = itemsToDenylist.map( (denylistItem) => `results.${denylistItem}` ); diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts index ec4248e99c..97e23660a9 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsPersistDenylist.ts @@ -6,6 +6,7 @@ import { UploadsState } from './uploadsSlice'; * Currently denylisting uploads slice entirely, see persist config in store.ts */ const itemsToDenylist: (keyof UploadsState)[] = []; +export const uploadsPersistDenylist: (keyof UploadsState)[] = []; export const uploadsDenylist = itemsToDenylist.map( (denylistItem) => `uploads.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts index 6e39733f1b..fcad240058 100644 --- a/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts +++ b/invokeai/frontend/web/src/features/gallery/store/uploadsSlice.ts @@ -21,7 +21,7 @@ type AdditionalUploadsState = { nextPage: number; }; -const initialUploadsState = +export const initialUploadsState = uploadsAdapter.getInitialState({ page: 0, pages: 0, diff --git a/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts b/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts index 96cf69f373..194fe50ca3 100644 --- a/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/lightbox/store/lightboxPersistDenylist.ts @@ -4,6 +4,9 @@ import { LightboxState } from './lightboxSlice'; * Lightbox slice persist denylist */ const itemsToDenylist: (keyof LightboxState)[] = ['isLightboxOpen']; +export const lightboxPersistDenylist: (keyof LightboxState)[] = [ + 'isLightboxOpen', +]; export const lightboxDenylist = itemsToDenylist.map( (denylistItem) => `lightbox.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts b/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts index f7f6507d93..ea73e5bb13 100644 --- a/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts +++ b/invokeai/frontend/web/src/features/lightbox/store/lightboxSlice.ts @@ -5,7 +5,7 @@ export interface LightboxState { isLightboxOpen: boolean; } -const initialLightboxState: LightboxState = { +export const initialLightboxState: LightboxState = { isLightboxOpen: false, }; diff --git a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx index b5783891ee..b97bf423e1 100644 --- a/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx +++ b/invokeai/frontend/web/src/features/nodes/components/panels/TopCenterPanel.tsx @@ -1,5 +1,5 @@ import { HStack } from '@chakra-ui/react'; -import { userInvoked } from 'app/store/middleware/listenerMiddleware/listeners/userInvoked'; +import { userInvoked } from 'app/store/actions'; import { useAppDispatch } from 'app/store/storeHooks'; import IAIButton from 'common/components/IAIButton'; import { memo, useCallback } from 'react'; diff --git a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts index 31d859ba8b..36da41f56c 100644 --- a/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/nodes/store/nodesPersistDenylist.ts @@ -4,6 +4,10 @@ import { NodesState } from './nodesSlice'; * Nodes slice persist denylist */ const itemsToDenylist: (keyof NodesState)[] = ['schema', 'invocationTemplates']; +export const nodesPersistDenylist: (keyof NodesState)[] = [ + 'schema', + 'invocationTemplates', +]; export const nodesDenylist = itemsToDenylist.map( (denylistItem) => `nodes.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx index 5532fab196..68d607c0fa 100644 --- a/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/ProcessButtons/InvokeButton.tsx @@ -1,6 +1,6 @@ import { Box } from '@chakra-ui/react'; import { readinessSelector } from 'app/selectors/readinessSelector'; -import { userInvoked } from 'app/store/middleware/listenerMiddleware/listeners/userInvoked'; +import { userInvoked } from 'app/store/actions'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAIButton, { IAIButtonProps } from 'common/components/IAIButton'; import IAIIconButton, { diff --git a/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx b/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx index 868da4c8b1..eb0340b0ee 100644 --- a/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx +++ b/invokeai/frontend/web/src/features/parameters/components/PromptInput/PromptInput.tsx @@ -15,7 +15,7 @@ import { activeTabNameSelector } from 'features/ui/store/uiSelectors'; import { isEqual } from 'lodash-es'; import { useHotkeys } from 'react-hotkeys-hook'; import { useTranslation } from 'react-i18next'; -import { userInvoked } from 'app/store/middleware/listenerMiddleware/listeners/userInvoked'; +import { userInvoked } from 'app/store/actions'; const promptInputSelector = createSelector( [(state: RootState) => state.generation, activeTabNameSelector], diff --git a/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts b/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts index 70f35aa564..ab3e77801e 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationPersistDenylist.ts @@ -4,6 +4,7 @@ import { GenerationState } from './generationSlice'; * Generation slice persist denylist */ const itemsToDenylist: (keyof GenerationState)[] = []; +export const generationPersistDenylist: (keyof GenerationState)[] = []; export const generationDenylist = itemsToDenylist.map( (denylistItem) => `generation.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts index d889dd88e1..51627ce24c 100644 --- a/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/generationSlice.ts @@ -38,9 +38,10 @@ export interface GenerationState { horizontalSymmetrySteps: number; verticalSymmetrySteps: number; isImageToImageEnabled: boolean; + model: string; } -const initialGenerationState: GenerationState = { +export const initialGenerationState: GenerationState = { cfgScale: 7.5, height: 512, img2imgStrength: 0.75, @@ -70,6 +71,7 @@ const initialGenerationState: GenerationState = { horizontalSymmetrySteps: 0, verticalSymmetrySteps: 0, isImageToImageEnabled: false, + model: '', }; const initialState: GenerationState = initialGenerationState; @@ -353,6 +355,9 @@ export const generationSlice = createSlice({ isImageToImageEnabledChanged: (state, action: PayloadAction) => { state.isImageToImageEnabled = action.payload; }, + modelSelected: (state, action: PayloadAction) => { + state.model = action.payload; + }, }, }); @@ -396,6 +401,7 @@ export const { setVerticalSymmetrySteps, initialImageChanged, isImageToImageEnabledChanged, + modelSelected, } = generationSlice.actions; export default generationSlice.reducer; diff --git a/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts index 947a136964..a6ba084c2e 100644 --- a/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/parameters/store/postprocessingPersistDenylist.ts @@ -4,6 +4,7 @@ import { PostprocessingState } from './postprocessingSlice'; * Postprocessing slice persist denylist */ const itemsToDenylist: (keyof PostprocessingState)[] = []; +export const postprocessingPersistDenylist: (keyof PostprocessingState)[] = []; export const postprocessingDenylist = itemsToDenylist.map( (denylistItem) => `postprocessing.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts b/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts index 60991d3673..399a474008 100644 --- a/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts +++ b/invokeai/frontend/web/src/features/parameters/store/postprocessingSlice.ts @@ -20,7 +20,7 @@ export interface PostprocessingState { upscalingStrength: number; } -const initialPostprocessingState: PostprocessingState = { +export const initialPostprocessingState: PostprocessingState = { codeformerFidelity: 0.75, facetoolStrength: 0.75, facetoolType: 'gfpgan', @@ -34,11 +34,9 @@ const initialPostprocessingState: PostprocessingState = { upscalingStrength: 0.75, }; -const initialState: PostprocessingState = initialPostprocessingState; - export const postprocessingSlice = createSlice({ name: 'postprocessing', - initialState, + initialState: initialPostprocessingState, reducers: { setFacetoolStrength: (state, action: PayloadAction) => { state.facetoolStrength = action.payload; diff --git a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx index d0ad89ba36..d6c7c154e0 100644 --- a/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx +++ b/invokeai/frontend/web/src/features/system/components/ModelSelect.tsx @@ -6,16 +6,22 @@ import { useTranslation } from 'react-i18next'; import { useAppDispatch, useAppSelector } from 'app/store/storeHooks'; import IAISelect from 'common/components/IAISelect'; import { - modelSelected, + // modelSelected, selectedModelSelector, + selectModelsById, selectModelsIds, } from '../store/modelSlice'; import { RootState } from 'app/store/store'; +import generationSlice, { + modelSelected, +} from 'features/parameters/store/generationSlice'; +import { generationSelector } from 'features/parameters/store/generationSelectors'; const selector = createSelector( - [(state: RootState) => state], - (state) => { - const selectedModel = selectedModelSelector(state); + [(state: RootState) => state, generationSelector], + (state, generation) => { + // const selectedModel = selectedModelSelector(state); + const selectedModel = selectModelsById(state, generation.model); const allModelNames = selectModelsIds(state); return { allModelNames, 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 0ca0b496fc..bf4004a79d 100644 --- a/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx +++ b/invokeai/frontend/web/src/features/system/components/SettingsModal/SettingsModal.tsx @@ -34,11 +34,11 @@ import { } from 'features/ui/store/uiSlice'; import { UIState } from 'features/ui/store/uiTypes'; import { isEqual } from 'lodash-es'; -import { persistor } from 'app/store/persistor'; import { ChangeEvent, cloneElement, ReactElement, useCallback } from 'react'; import { useTranslation } from 'react-i18next'; import { VALID_LOG_LEVELS } from 'app/logging/useLogger'; import { LogLevelName } from 'roarr'; +import { LOCALSTORAGE_KEYS, LOCALSTORAGE_PREFIX } from 'app/store/constants'; const selector = createSelector( [systemSelector, uiSelector], @@ -119,15 +119,18 @@ const SettingsModal = ({ children }: SettingsModalProps) => { shouldLogToConsole, } = useAppSelector(selector); - /** - * Resets localstorage, then opens a secondary modal informing user to - * refresh their browser. - * */ const handleClickResetWebUI = useCallback(() => { - persistor.purge().then(() => { - onSettingsModalClose(); - onRefreshModalOpen(); + // Only remove our keys + Object.keys(window.localStorage).forEach((key) => { + if ( + LOCALSTORAGE_KEYS.includes(key) || + key.startsWith(LOCALSTORAGE_PREFIX) + ) { + localStorage.removeItem(key); + } }); + onSettingsModalClose(); + onRefreshModalOpen(); }, [onSettingsModalClose, onRefreshModalOpen]); const handleLogLevelChanged = useCallback( diff --git a/invokeai/frontend/web/src/features/system/store/configSlice.ts b/invokeai/frontend/web/src/features/system/store/configSlice.ts index d668a59574..b773692908 100644 --- a/invokeai/frontend/web/src/features/system/store/configSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/configSlice.ts @@ -3,7 +3,7 @@ import { createSlice } from '@reduxjs/toolkit'; import { AppConfig, PartialAppConfig } from 'app/types/invokeai'; import { merge } from 'lodash-es'; -const initialConfigState: AppConfig = { +export const initialConfigState: AppConfig = { shouldTransformUrls: false, shouldFetchImages: false, disabledTabs: [], diff --git a/invokeai/frontend/web/src/features/system/store/modelSlice.ts b/invokeai/frontend/web/src/features/system/store/modelSlice.ts index cb1cf05328..88ca71f2ff 100644 --- a/invokeai/frontend/web/src/features/system/store/modelSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/modelSlice.ts @@ -27,12 +27,13 @@ export type ModelsState = typeof initialModelsState; export const modelsSlice = createSlice({ name: 'models', - initialState: initialModelsState, + initialState: modelsAdapter.getInitialState(), + // initialState: initialModelsState, reducers: { modelAdded: modelsAdapter.upsertOne, - modelSelected: (state, action: PayloadAction) => { - state.selectedModelName = action.payload; - }, + // modelSelected: (state, action: PayloadAction) => { + // state.selectedModelName = action.payload; + // }, }, extraReducers(builder) { /** @@ -44,18 +45,18 @@ export const modelsSlice = createSlice({ // If the current selected model is `''` or isn't actually in the list of models, // choose a random model - if ( - !state.selectedModelName || - !keys(models).includes(state.selectedModelName) - ) { - const randomModel = sample(models); + // if ( + // !state.selectedModelName || + // !keys(models).includes(state.selectedModelName) + // ) { + // const randomModel = sample(models); - if (randomModel) { - state.selectedModelName = randomModel.name; - } else { - state.selectedModelName = ''; - } - } + // if (randomModel) { + // state.selectedModelName = randomModel.name; + // } else { + // state.selectedModelName = ''; + // } + // } }); }, }); @@ -75,6 +76,9 @@ export const { selectTotal: selectModelsTotal, } = modelsAdapter.getSelectors((state) => state.models); -export const { modelAdded, modelSelected } = modelsSlice.actions; +export const { + modelAdded, + // modelSelected +} = modelsSlice.actions; export default modelsSlice.reducer; diff --git a/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts b/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts index f374948e39..5b36a3f196 100644 --- a/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/system/store/modelsPersistDenylist.ts @@ -4,6 +4,7 @@ import { ModelsState } from './modelSlice'; * Models slice persist denylist */ const itemsToDenylist: (keyof ModelsState)[] = ['entities', 'ids']; +export const modelsPersistDenylist: (keyof ModelsState)[] = ['entities', 'ids']; export const modelsDenylist = itemsToDenylist.map( (denylistItem) => `models.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts index 8a4d381775..70284e831a 100644 --- a/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/system/store/systemPersistDenylist.ts @@ -21,6 +21,25 @@ const itemsToDenylist: (keyof SystemState)[] = [ 'wereModelsReceived', 'wasSchemaParsed', ]; +export const systemPersistDenylist: (keyof SystemState)[] = [ + 'currentIteration', + 'currentStatus', + 'currentStep', + 'isCancelable', + 'isConnected', + 'isESRGANAvailable', + 'isGFPGANAvailable', + 'isProcessing', + 'socketId', + 'totalIterations', + 'totalSteps', + 'openModel', + 'isCancelScheduled', + 'progressImage', + 'wereModelsReceived', + 'wasSchemaParsed', + 'isPersisted', +]; export const systemDenylist = itemsToDenylist.map( (denylistItem) => `system.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/system/store/systemSlice.ts b/invokeai/frontend/web/src/features/system/store/systemSlice.ts index 16f118855f..025cebf0a6 100644 --- a/invokeai/frontend/web/src/features/system/store/systemSlice.ts +++ b/invokeai/frontend/web/src/features/system/store/systemSlice.ts @@ -89,9 +89,10 @@ export interface SystemState { * TODO: get this from backend */ infillMethods: InfillMethod[]; + isPersisted: boolean; } -const initialSystemState: SystemState = { +export const initialSystemState: SystemState = { isConnected: false, isProcessing: false, shouldDisplayGuides: true, @@ -121,6 +122,7 @@ const initialSystemState: SystemState = { statusTranslationKey: 'common.statusDisconnected', canceledSession: '', infillMethods: ['tile', 'patchmatch'], + isPersisted: false, }; export const systemSlice = createSlice({ @@ -259,6 +261,9 @@ export const systemSlice = createSlice({ shouldLogToConsoleChanged: (state, action: PayloadAction) => { state.shouldLogToConsole = action.payload; }, + isPersistedChanged: (state, action: PayloadAction) => { + state.isPersisted = action.payload; + }, }, extraReducers(builder) { /** @@ -476,6 +481,7 @@ export const { subscribedNodeIdsSet, consoleLogLevelChanged, shouldLogToConsoleChanged, + isPersistedChanged, } = systemSlice.actions; export default systemSlice.reducer; diff --git a/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts b/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts index 4e72d1dce9..527e0b1740 100644 --- a/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/hotkeysSlice.ts @@ -6,15 +6,13 @@ type HotkeysState = { shift: boolean; }; -const initialHotkeysState: HotkeysState = { +export const initialHotkeysState: HotkeysState = { shift: false, }; -const initialState: HotkeysState = initialHotkeysState; - export const hotkeysSlice = createSlice({ name: 'hotkeys', - initialState, + initialState: initialHotkeysState, reducers: { shiftKeyPressed: (state, action: PayloadAction) => { state.shift = action.payload; diff --git a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts index ae357e7899..7f469e1f1a 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiPersistDenylist.ts @@ -4,6 +4,9 @@ import { UIState } from './uiTypes'; * UI slice persist denylist */ const itemsToDenylist: (keyof UIState)[] = ['floatingProgressImageRect']; +export const uiPersistDenylist: (keyof UIState)[] = [ + 'floatingProgressImageRect', +]; export const uiDenylist = itemsToDenylist.map( (denylistItem) => `ui.${denylistItem}` diff --git a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts index 11abf6a20d..4a21a729d7 100644 --- a/invokeai/frontend/web/src/features/ui/store/uiSlice.ts +++ b/invokeai/frontend/web/src/features/ui/store/uiSlice.ts @@ -4,7 +4,7 @@ import { setActiveTabReducer } from './extraReducers'; import { InvokeTabName, tabMap } from './tabMap'; import { AddNewModelType, Coordinates, Rect, UIState } from './uiTypes'; -const initialUIState: UIState = { +export const initialUIState: UIState = { activeTab: 0, currentTheme: 'dark', parametersPanelScrollPosition: 0, @@ -26,11 +26,9 @@ const initialUIState: UIState = { shouldAutoShowProgressImages: false, }; -const initialState: UIState = initialUIState; - export const uiSlice = createSlice({ name: 'ui', - initialState, + initialState: initialUIState, reducers: { setActiveTab: (state, action: PayloadAction) => { setActiveTabReducer(state, action.payload); diff --git a/invokeai/frontend/web/src/i18n.ts b/invokeai/frontend/web/src/i18n.ts index ed71d583b3..f28365db20 100644 --- a/invokeai/frontend/web/src/i18n.ts +++ b/invokeai/frontend/web/src/i18n.ts @@ -3,7 +3,8 @@ import LanguageDetector from 'i18next-browser-languagedetector'; import Backend from 'i18next-http-backend'; import { initReactI18next } from 'react-i18next'; -import translationEN from '../public/locales/en.json'; +import translationEN from '../dist/locales/en.json'; +import { LOCALSTORAGE_PREFIX } from 'app/store/constants'; if (import.meta.env.MODE === 'package') { i18n.use(initReactI18next).init({ @@ -20,7 +21,11 @@ if (import.meta.env.MODE === 'package') { } else { i18n .use(Backend) - .use(LanguageDetector) + .use( + new LanguageDetector(null, { + lookupLocalStorage: `${LOCALSTORAGE_PREFIX}lng`, + }) + ) .use(initReactI18next) .init({ fallbackLng: 'en', diff --git a/invokeai/frontend/web/src/services/events/middleware.ts b/invokeai/frontend/web/src/services/events/middleware.ts index 9ece44665a..bd1d60099a 100644 --- a/invokeai/frontend/web/src/services/events/middleware.ts +++ b/invokeai/frontend/web/src/services/events/middleware.ts @@ -8,11 +8,7 @@ import { import { socketSubscribed, socketUnsubscribed } from './actions'; import { AppThunkDispatch, RootState } from 'app/store/store'; import { getTimestamp } from 'common/util/getTimestamp'; -import { - sessionInvoked, - isFulfilledSessionCreatedAction, - sessionCreated, -} from 'services/thunks/session'; +import { sessionInvoked, sessionCreated } from 'services/thunks/session'; import { OpenAPI } from 'services/api'; import { setEventListeners } from 'services/events/util/setEventListeners'; import { log } from 'app/logging/useLogger'; diff --git a/invokeai/frontend/web/src/services/thunks/session.ts b/invokeai/frontend/web/src/services/thunks/session.ts index bc26ee7aba..dca4134886 100644 --- a/invokeai/frontend/web/src/services/thunks/session.ts +++ b/invokeai/frontend/web/src/services/thunks/session.ts @@ -1,71 +1,10 @@ import { createAppAsyncThunk } from 'app/store/storeUtils'; import { SessionsService } from 'services/api'; -import { buildLinearGraph as buildGenerateGraph } from 'features/nodes/util/buildLinearGraph'; -import { buildCanvasGraphAndBlobs } from 'features/nodes/util/buildCanvasGraph'; -import { isAnyOf, isFulfilled } from '@reduxjs/toolkit'; -import { buildNodesGraph } from 'features/nodes/util/buildNodesGraph'; import { log } from 'app/logging/useLogger'; import { serializeError } from 'serialize-error'; const sessionLog = log.child({ namespace: 'session' }); -// export const generateGraphBuilt = createAppAsyncThunk( -// 'api/generateGraphBuilt', -// async (_, { dispatch, getState, rejectWithValue }) => { -// try { -// const graph = buildGenerateGraph(getState()); -// dispatch(sessionCreated({ graph })); -// return graph; -// } catch (err: any) { -// sessionLog.error( -// { error: serializeError(err) }, -// 'Problem building graph' -// ); -// return rejectWithValue(err.message); -// } -// } -// ); - -// export const nodesGraphBuilt = createAppAsyncThunk( -// 'api/nodesGraphBuilt', -// async (_, { dispatch, getState, rejectWithValue }) => { -// try { -// const graph = buildNodesGraph(getState()); -// dispatch(sessionCreated({ graph })); -// return graph; -// } catch (err: any) { -// sessionLog.error( -// { error: serializeError(err) }, -// 'Problem building graph' -// ); -// return rejectWithValue(err.message); -// } -// } -// ); - -// export const canvasGraphBuilt = createAppAsyncThunk( -// 'api/canvasGraphBuilt', -// async (_, { dispatch, getState, rejectWithValue }) => { -// try { -// const graph = await buildCanvasGraph(getState()); -// dispatch(sessionCreated({ graph })); -// return graph; -// } catch (err: any) { -// sessionLog.error( -// { error: serializeError(err) }, -// 'Problem building graph' -// ); -// return rejectWithValue(err.message); -// } -// } -// ); - -// export const isFulfilledAnyGraphBuilt = isAnyOf( -// generateGraphBuilt.fulfilled, -// nodesGraphBuilt.fulfilled, -// canvasGraphBuilt.fulfilled -// ); - type SessionCreatedArg = { graph: Parameters< (typeof SessionsService)['createSession'] @@ -96,11 +35,6 @@ export const sessionCreated = createAppAsyncThunk( } ); -/** - * Function to check if an action is a fulfilled `SessionsService.createSession()` thunk - */ -export const isFulfilledSessionCreatedAction = isFulfilled(sessionCreated); - type NodeAddedArg = Parameters<(typeof SessionsService)['addNode']>[0]; /** diff --git a/invokeai/frontend/web/yarn.lock b/invokeai/frontend/web/yarn.lock index 3901903bd4..18a08a01b5 100644 --- a/invokeai/frontend/web/yarn.lock +++ b/invokeai/frontend/web/yarn.lock @@ -5692,6 +5692,11 @@ redux-persist@^6.0.0: resolved "https://registry.yarnpkg.com/redux-persist/-/redux-persist-6.0.0.tgz#b4d2972f9859597c130d40d4b146fecdab51b3a8" integrity sha512-71LLMbUq2r02ng2We9S215LtPu3fY0KgaGE0k8WRgl6RkqxtGfl7HUozz1Dftwsb0D/5mZ8dwAaPbtnzfvbEwQ== +redux-remember@^3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/redux-remember/-/redux-remember-3.2.1.tgz#e58600336ac7341c56dfe9d69d95db234a7c404e" + integrity sha512-ep2E5KOJDGmrvbAuHVfmVpnuftqhJ2um6VpHw/iWa7WvAIFcPq/B678n51NBd/g8BWnNdQ5131cDRKWrRd041Q== + redux-thunk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.4.2.tgz#b9d05d11994b99f7a91ea223e8b04cf0afa5ef3b"