From 4602f962602a4316ddf357e551edeb28afc4326f Mon Sep 17 00:00:00 2001 From: Jixun Wu Date: Sat, 3 Jun 2023 14:09:11 +0100 Subject: [PATCH] feat: setup redux store for settings --- package.json | 2 ++ pnpm-lock.yaml | 17 +++++++++ src/Loader.tsx | 24 +++++++++++++ src/features/settings/persistSettings.ts | 45 ++++++++++++++++++++++++ src/features/settings/settingsSlice.ts | 31 ++++++++++++++++ src/main.tsx | 16 ++------- src/store.ts | 2 ++ src/util/logUtils.ts | 20 +++++++++++ src/util/objects.ts | 7 ++++ 9 files changed, 150 insertions(+), 14 deletions(-) create mode 100644 src/Loader.tsx create mode 100644 src/features/settings/persistSettings.ts create mode 100644 src/features/settings/settingsSlice.ts create mode 100644 src/util/objects.ts diff --git a/package.json b/package.json index 819a2b8..af15912 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,9 @@ "@jixun/libparakeet": "0.1.1", "@reduxjs/toolkit": "^1.9.5", "framer-motion": "^10.12.16", + "immer": "^10.0.2", "nanoid": "^4.0.2", + "radash": "^10.8.1", "react": "^18.2.0", "react-dom": "^18.2.0", "react-dropzone": "^14.2.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0fa7b36..29dfd3c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -22,9 +22,15 @@ dependencies: framer-motion: specifier: ^10.12.16 version: 10.12.16(react-dom@18.2.0)(react@18.2.0) + immer: + specifier: ^10.0.2 + version: 10.0.2 nanoid: specifier: ^4.0.2 version: 4.0.2 + radash: + specifier: ^10.8.1 + version: 10.8.1 react: specifier: ^18.2.0 version: 18.2.0 @@ -5483,6 +5489,11 @@ packages: engines: { node: '>= 4' } dev: true + /immer@10.0.2: + resolution: + { integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA== } + dev: false + /immer@9.0.21: resolution: { integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== } @@ -6608,6 +6619,12 @@ packages: { integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== } dev: true + /radash@10.8.1: + resolution: + { integrity: sha512-NzYo3XgM9Tzjf5iFPIMG2l5+LSOCi2H7Axe3Ry/1PrhlvuqxUoiLsmcTBtw4CfKtzy5Fzo79STiEj9JZWMfDQg== } + engines: { node: '>=14.18.0' } + dev: false + /randombytes@2.1.0: resolution: { integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== } diff --git a/src/Loader.tsx b/src/Loader.tsx new file mode 100644 index 0000000..a728531 --- /dev/null +++ b/src/Loader.tsx @@ -0,0 +1,24 @@ +import React, { useEffect } from 'react'; +import App from './App'; + +import { ChakraProvider } from '@chakra-ui/react'; +import { Provider } from 'react-redux'; +import { theme } from './theme'; +import { persistSettings } from './features/settings/persistSettings'; +import type { AppStore } from './store'; + +export function Loader({ store }: { store: AppStore }) { + useEffect(() => { + return persistSettings(store); + }, [store]); + + return ( + + + + + + + + ); +} diff --git a/src/features/settings/persistSettings.ts b/src/features/settings/persistSettings.ts new file mode 100644 index 0000000..420266b --- /dev/null +++ b/src/features/settings/persistSettings.ts @@ -0,0 +1,45 @@ +import { debounce } from 'radash'; +import { produce } from 'immer'; + +import type { AppStore } from '~/store'; +import { SettingsState, settingsSlice, updateSettings } from './settingsSlice'; +import { enumObject } from '~/util/objects'; +import { getLogger } from '~/util/logUtils'; + +const DEFAULT_STORAGE_KEY = 'um-react-settings'; + +function mergeSettings(settings: SettingsState): SettingsState { + return produce(settingsSlice.getInitialState(), (draft) => { + for (const [k, v] of enumObject(settings.qmc2?.keys)) { + if (typeof v === 'string') { + draft.qmc2.keys[k] = v; + } + } + }); +} + +export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KEY) { + let lastSettings: unknown; + + try { + const loadedSettings: SettingsState = JSON.parse(localStorage.getItem(storageKey) ?? ''); + if (loadedSettings) { + const mergedSettings = mergeSettings(loadedSettings); + store.dispatch(updateSettings(mergedSettings)); + getLogger().debug('settings loaded'); + } + } catch { + // load failed, ignore. + } + + return store.subscribe( + debounce({ delay: 150 }, () => { + const currentSettings = store.getState().settings; + if (lastSettings !== currentSettings) { + lastSettings = currentSettings; + localStorage.setItem(storageKey, JSON.stringify(currentSettings)); + getLogger().debug('settings saved'); + } + }) + ); +} diff --git a/src/features/settings/settingsSlice.ts b/src/features/settings/settingsSlice.ts new file mode 100644 index 0000000..b08a340 --- /dev/null +++ b/src/features/settings/settingsSlice.ts @@ -0,0 +1,31 @@ +import { createSlice } from '@reduxjs/toolkit'; +import type { PayloadAction } from '@reduxjs/toolkit'; + +export interface QMCSettings { + keys: Record; // { [fileName]: ekey } +} + +export interface SettingsState { + qmc2: QMCSettings; +} + +const initialState: SettingsState = { + qmc2: { keys: {} }, +}; + +export const settingsSlice = createSlice({ + name: 'settings', + initialState, + reducers: { + updateSettings: (_state, { payload }: PayloadAction) => { + return payload; + }, + resetConfig: () => { + return initialState; + }, + }, +}); + +export const { updateSettings, resetConfig } = settingsSlice.actions; + +export default settingsSlice.reducer; diff --git a/src/main.tsx b/src/main.tsx index dad48ae..469e6dc 100644 --- a/src/main.tsx +++ b/src/main.tsx @@ -1,21 +1,9 @@ -import React from 'react'; import ReactDOM from 'react-dom/client'; -import App from './App'; -import { ChakraProvider } from '@chakra-ui/react'; -import { Provider } from 'react-redux'; import { setupStore } from './store'; -import { theme } from './theme'; +import { Loader } from './Loader'; // Private to this file only. const store = setupStore(); -ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render( - - - - - - - -); +ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(); diff --git a/src/store.ts b/src/store.ts index 542db54..b97f8a6 100644 --- a/src/store.ts +++ b/src/store.ts @@ -1,8 +1,10 @@ import { PreloadedState, combineReducers, configureStore } from '@reduxjs/toolkit'; import fileListingReducer from './features/file-listing/fileListingSlice'; +import settingsReducer from './features/settings/settingsSlice'; const rootReducer = combineReducers({ fileListing: fileListingReducer, + settings: settingsReducer, }); export const setupStore = (preloadedState?: PreloadedState) => diff --git a/src/util/logUtils.ts b/src/util/logUtils.ts index e37609a..29430b5 100644 --- a/src/util/logUtils.ts +++ b/src/util/logUtils.ts @@ -23,3 +23,23 @@ export function withGroupedLogs(label: string, fn: () => R): R { ); } } + +const noop = (..._args: unknown[]) => { + // noop +}; + +const dummyLogger = { + log: noop, + info: noop, + warn: noop, + debug: noop, + trace: noop, +}; + +export function getLogger() { + if (import.meta.env.ENABLE_PERF_LOG === '1') { + return window.console; + } else { + return dummyLogger; + } +} diff --git a/src/util/objects.ts b/src/util/objects.ts new file mode 100644 index 0000000..77f700b --- /dev/null +++ b/src/util/objects.ts @@ -0,0 +1,7 @@ +export function* enumObject(obj: Record | null | void): Generator<[string, T]> { + if (obj && typeof obj === 'object') { + for (const key in obj) { + yield [key, obj[key]]; + } + } +}