Compare commits
No commits in common. "3a2a31f3722dacfad426b2d07bd9edda90e16a16" and "c4dbaa4b73c9aa5f427e8dcbbb32b7703bff92f0" have entirely different histories.
3a2a31f372
...
c4dbaa4b73
@ -23,9 +23,7 @@
|
|||||||
"@jixun/libparakeet": "0.1.1",
|
"@jixun/libparakeet": "0.1.1",
|
||||||
"@reduxjs/toolkit": "^1.9.5",
|
"@reduxjs/toolkit": "^1.9.5",
|
||||||
"framer-motion": "^10.12.16",
|
"framer-motion": "^10.12.16",
|
||||||
"immer": "^10.0.2",
|
|
||||||
"nanoid": "^4.0.2",
|
"nanoid": "^4.0.2",
|
||||||
"radash": "^10.8.1",
|
|
||||||
"react": "^18.2.0",
|
"react": "^18.2.0",
|
||||||
"react-dom": "^18.2.0",
|
"react-dom": "^18.2.0",
|
||||||
"react-dropzone": "^14.2.3",
|
"react-dropzone": "^14.2.3",
|
||||||
|
@ -22,15 +22,9 @@ dependencies:
|
|||||||
framer-motion:
|
framer-motion:
|
||||||
specifier: ^10.12.16
|
specifier: ^10.12.16
|
||||||
version: 10.12.16(react-dom@18.2.0)(react@18.2.0)
|
version: 10.12.16(react-dom@18.2.0)(react@18.2.0)
|
||||||
immer:
|
|
||||||
specifier: ^10.0.2
|
|
||||||
version: 10.0.2
|
|
||||||
nanoid:
|
nanoid:
|
||||||
specifier: ^4.0.2
|
specifier: ^4.0.2
|
||||||
version: 4.0.2
|
version: 4.0.2
|
||||||
radash:
|
|
||||||
specifier: ^10.8.1
|
|
||||||
version: 10.8.1
|
|
||||||
react:
|
react:
|
||||||
specifier: ^18.2.0
|
specifier: ^18.2.0
|
||||||
version: 18.2.0
|
version: 18.2.0
|
||||||
@ -5489,11 +5483,6 @@ packages:
|
|||||||
engines: { node: '>= 4' }
|
engines: { node: '>= 4' }
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/immer@10.0.2:
|
|
||||||
resolution:
|
|
||||||
{ integrity: sha512-Rx3CqeqQ19sxUtYV9CU911Vhy8/721wRFnJv3REVGWUmoAcIwzifTsdmJte/MV+0/XpM35LZdQMBGkRIoLPwQA== }
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/immer@9.0.21:
|
/immer@9.0.21:
|
||||||
resolution:
|
resolution:
|
||||||
{ integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== }
|
{ integrity: sha512-bc4NBHqOqSfRW7POMkHd51LvClaeMXpm8dx0e8oE2GORbq5aRK7Bxl4FyzVLdGtLmvLKL7BTDBG5ACQm4HWjTA== }
|
||||||
@ -6619,12 +6608,6 @@ packages:
|
|||||||
{ integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== }
|
{ integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A== }
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
/radash@10.8.1:
|
|
||||||
resolution:
|
|
||||||
{ integrity: sha512-NzYo3XgM9Tzjf5iFPIMG2l5+LSOCi2H7Axe3Ry/1PrhlvuqxUoiLsmcTBtw4CfKtzy5Fzo79STiEj9JZWMfDQg== }
|
|
||||||
engines: { node: '>=14.18.0' }
|
|
||||||
dev: false
|
|
||||||
|
|
||||||
/randombytes@2.1.0:
|
/randombytes@2.1.0:
|
||||||
resolution:
|
resolution:
|
||||||
{ integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== }
|
{ integrity: sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ== }
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { Box, Center, Container } from '@chakra-ui/react';
|
import { Box, Center, Container } from '@chakra-ui/react';
|
||||||
import { SelectFile } from './SelectFile';
|
import { SelectFile } from './SelectFile';
|
||||||
|
|
||||||
import { FileListing } from '~/features/file-listing/FileListing';
|
import { FileListing } from './features/file-listing/FileListing';
|
||||||
import { Footer } from './Footer';
|
import { Footer } from './Footer';
|
||||||
|
|
||||||
function App() {
|
function App() {
|
@ -2,8 +2,8 @@ import { useDropzone } from 'react-dropzone';
|
|||||||
import { Box, Text } from '@chakra-ui/react';
|
import { Box, Text } from '@chakra-ui/react';
|
||||||
import { UnlockIcon } from '@chakra-ui/icons';
|
import { UnlockIcon } from '@chakra-ui/icons';
|
||||||
|
|
||||||
import { useAppDispatch } from '~/hooks';
|
import { useAppDispatch } from './hooks';
|
||||||
import { addNewFile, processFile } from '~/features/file-listing/fileListingSlice';
|
import { addNewFile, processFile } from './features/file-listing/fileListingSlice';
|
||||||
import { nanoid } from 'nanoid';
|
import { nanoid } from 'nanoid';
|
||||||
|
|
||||||
export function SelectFile() {
|
export function SelectFile() {
|
@ -1,5 +1,5 @@
|
|||||||
import { renderWithProviders, screen, waitFor } from '~/test-utils/test-helper';
|
import { renderWithProviders, screen, waitFor } from '~/test-utils/test-helper';
|
||||||
import App from '~/components/App';
|
import App from '~/App';
|
||||||
|
|
||||||
vi.mock('../decrypt-worker/client', () => {
|
vi.mock('../decrypt-worker/client', () => {
|
||||||
return {
|
return {
|
||||||
|
@ -1,23 +0,0 @@
|
|||||||
import { 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 { setupStore } from '~/store';
|
|
||||||
|
|
||||||
// Private to this file only.
|
|
||||||
const store = setupStore();
|
|
||||||
|
|
||||||
export function AppRoot() {
|
|
||||||
useEffect(() => persistSettings(store), []);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ChakraProvider theme={theme}>
|
|
||||||
<Provider store={store}>
|
|
||||||
<App />
|
|
||||||
</Provider>
|
|
||||||
</ChakraProvider>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,7 +1,7 @@
|
|||||||
import { VStack } from '@chakra-ui/react';
|
import { VStack } from '@chakra-ui/react';
|
||||||
|
|
||||||
import { selectFiles } from './fileListingSlice';
|
import { selectFiles } from './fileListingSlice';
|
||||||
import { useAppSelector } from '~/hooks';
|
import { useAppSelector } from '../../hooks';
|
||||||
import { FileRow } from './FileRow';
|
import { FileRow } from './FileRow';
|
||||||
|
|
||||||
export function FileListing() {
|
export function FileListing() {
|
||||||
|
@ -1,45 +0,0 @@
|
|||||||
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');
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,31 +0,0 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
|
||||||
import type { PayloadAction } from '@reduxjs/toolkit';
|
|
||||||
|
|
||||||
export interface QMCSettings {
|
|
||||||
keys: Record<string, string>; // { [fileName]: ekey }
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface SettingsState {
|
|
||||||
qmc2: QMCSettings;
|
|
||||||
}
|
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
|
||||||
qmc2: { keys: {} },
|
|
||||||
};
|
|
||||||
|
|
||||||
export const settingsSlice = createSlice({
|
|
||||||
name: 'settings',
|
|
||||||
initialState,
|
|
||||||
reducers: {
|
|
||||||
updateSettings: (_state, { payload }: PayloadAction<SettingsState>) => {
|
|
||||||
return payload;
|
|
||||||
},
|
|
||||||
resetConfig: () => {
|
|
||||||
return initialState;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
export const { updateSettings, resetConfig } = settingsSlice.actions;
|
|
||||||
|
|
||||||
export default settingsSlice.reducer;
|
|
15
src/main.tsx
15
src/main.tsx
@ -1,10 +1,21 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import ReactDOM from 'react-dom/client';
|
import ReactDOM from 'react-dom/client';
|
||||||
|
import App from './App';
|
||||||
|
|
||||||
import { AppRoot } from './components/AppRoot';
|
import { ChakraProvider } from '@chakra-ui/react';
|
||||||
|
import { Provider } from 'react-redux';
|
||||||
|
import { setupStore } from './store';
|
||||||
|
import { theme } from './theme';
|
||||||
|
|
||||||
|
// Private to this file only.
|
||||||
|
const store = setupStore();
|
||||||
|
|
||||||
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
|
||||||
<React.StrictMode>
|
<React.StrictMode>
|
||||||
<AppRoot />
|
<ChakraProvider theme={theme}>
|
||||||
|
<Provider store={store}>
|
||||||
|
<App />
|
||||||
|
</Provider>
|
||||||
|
</ChakraProvider>
|
||||||
</React.StrictMode>
|
</React.StrictMode>
|
||||||
);
|
);
|
||||||
|
@ -1,10 +1,8 @@
|
|||||||
import { PreloadedState, combineReducers, configureStore } from '@reduxjs/toolkit';
|
import { PreloadedState, combineReducers, configureStore } from '@reduxjs/toolkit';
|
||||||
import fileListingReducer from './features/file-listing/fileListingSlice';
|
import fileListingReducer from './features/file-listing/fileListingSlice';
|
||||||
import settingsReducer from './features/settings/settingsSlice';
|
|
||||||
|
|
||||||
const rootReducer = combineReducers({
|
const rootReducer = combineReducers({
|
||||||
fileListing: fileListingReducer,
|
fileListing: fileListingReducer,
|
||||||
settings: settingsReducer,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setupStore = (preloadedState?: PreloadedState<RootState>) =>
|
export const setupStore = (preloadedState?: PreloadedState<RootState>) =>
|
||||||
|
@ -3,7 +3,7 @@ import { nextTickAsync } from '../nextTick';
|
|||||||
|
|
||||||
class SimpleQueue<T, R = void> extends ConcurrentQueue<T> {
|
class SimpleQueue<T, R = void> extends ConcurrentQueue<T> {
|
||||||
handler(_item: T): Promise<R> {
|
handler(_item: T): Promise<R> {
|
||||||
throw new Error('Method not overridden');
|
throw new Error('Method not overriden');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +39,7 @@ test('should be able to process the queue within limit', async () => {
|
|||||||
await promises[i];
|
await promises[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait till all fulfilled
|
// Wait till all fullfilled
|
||||||
while (queuedResolver.length !== 5) {
|
while (queuedResolver.length !== 5) {
|
||||||
await nextTickAsync();
|
await nextTickAsync();
|
||||||
}
|
}
|
||||||
@ -85,13 +85,13 @@ test('it should move on to the next item in the queue once failed', async () =>
|
|||||||
promises.push(queue.add(4));
|
promises.push(queue.add(4));
|
||||||
promises.push(queue.add(5));
|
promises.push(queue.add(5));
|
||||||
|
|
||||||
// Let first 2 be fulfilled
|
// Let first 2 be fullfilled
|
||||||
for (let i = 0; i < 2; i++) {
|
for (let i = 0; i < 2; i++) {
|
||||||
queuedResolver[i]();
|
queuedResolver[i]();
|
||||||
await promises[i];
|
await promises[i];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wait till all fulfilled
|
// Wait till all fullfilled
|
||||||
while (queuedResolver.length !== 4) {
|
while (queuedResolver.length !== 4) {
|
||||||
await nextTickAsync();
|
await nextTickAsync();
|
||||||
}
|
}
|
||||||
|
@ -23,23 +23,3 @@ export function withGroupedLogs<R = unknown>(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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
export function* enumObject<T>(obj: Record<string, T> | null | void): Generator<[string, T]> {
|
|
||||||
if (obj && typeof obj === 'object') {
|
|
||||||
for (const key in obj) {
|
|
||||||
yield [key, obj[key]];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue
Block a user