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]];
+ }
+ }
+}