diff --git a/src/features/settings/Settings.tsx b/src/features/settings/Settings.tsx
index 55ee431..8c31aed 100644
--- a/src/features/settings/Settings.tsx
+++ b/src/features/settings/Settings.tsx
@@ -1,36 +1,40 @@
import {
+ Box,
Button,
+ Center,
Flex,
+ HStack,
Menu,
MenuButton,
MenuItem,
MenuList,
Portal,
+ Spacer,
Tab,
TabList,
TabPanel,
TabPanels,
Tabs,
Text,
+ VStack,
useBreakpointValue,
} from '@chakra-ui/react';
import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
import { useState } from 'react';
import { MdExpandMore, MdMenu } from 'react-icons/md';
+import { useAppDispatch } from '~/hooks';
+import { commitStagingChange, discardStagingChanges } from './settingsSlice';
const TABS: { name: string; Tab: () => JSX.Element }[] = [
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
{
name: '其它/待定',
- Tab: () => (
-
- 这里空空如也~
-
- ),
+ Tab: () => 这里空空如也~,
},
];
export function Settings() {
+ const dispatch = useAppDispatch();
const isLargeWidthDevice =
useBreakpointValue({
base: false,
@@ -41,6 +45,8 @@ export function Settings() {
const handleTabChange = (idx: number) => {
setTabIndex(idx);
};
+ const handleResetSettings = () => dispatch(discardStagingChanges());
+ const handleApplySettings = () => dispatch(commitStagingChange());
return (
@@ -86,7 +92,26 @@ export function Settings() {
{TABS.map(({ name, Tab }) => (
-
+
+
+
+
+
+
+
+
+ 设置会在保存后生效。
+
+
+
+
+
+
+
+
+
))}
diff --git a/src/features/settings/panels/PanelQMCv2Key.tsx b/src/features/settings/panels/PanelQMCv2Key.tsx
index 1be0f5b..a165b16 100644
--- a/src/features/settings/panels/PanelQMCv2Key.tsx
+++ b/src/features/settings/panels/PanelQMCv2Key.tsx
@@ -2,7 +2,6 @@ import {
Box,
Button,
ButtonGroup,
- Center,
Flex,
HStack,
Heading,
@@ -19,84 +18,34 @@ import {
MenuDivider,
MenuItem,
MenuList,
- Spacer,
- TabPanel,
Text,
VStack,
- useToast,
} from '@chakra-ui/react';
import { useDispatch, useSelector } from 'react-redux';
-import { selectQM2CSettings, updateQMC2Keys } from '../settingsSlice';
-import React, { useEffect, useState } from 'react';
-import { nanoid } from 'nanoid';
-import { produce } from 'immer';
-import { MdAdd, MdAndroid, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
-import { objectify } from 'radash';
-
-interface InternalQMCKeys {
- id: string;
- name: string;
- key: string;
-}
+import { qmc2AddKey, qmc2ClearKeys, qmc2DeleteKey, qmc2UpdateKey } from '../settingsSlice';
+import { selectStagingQMCv2Settings } from '../settingsSelector';
+import React from 'react';
+import { MdAdd, MdAndroid, MdDelete, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
export function PanelQMCv2Key() {
- const toast = useToast();
const dispatch = useDispatch();
- const qmcSettings = useSelector(selectQM2CSettings);
- const [isModified, setIsModified] = useState(false);
- const [qmcKeys, setQMCKeys] = useState([]);
- const resetQmcKeys = () => {
- const result: InternalQMCKeys[] = [];
- for (const [name, key] of Object.entries(qmcSettings.keys)) {
- result.push({ id: name, name, key });
- }
- setQMCKeys(result);
- };
- const addRow = () => {
- setIsModified(true);
- setQMCKeys((prev) => [...prev, { id: nanoid(), key: '', name: '' }]);
- };
- const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent) => {
- setIsModified(true);
- setQMCKeys((prev) =>
- produce(prev, (draft) => {
- const item = draft.find((item) => item.id === id);
- if (item) {
- item[prop] = e.target.value;
- }
- })
- );
- };
- const applyChanges = () => {
- dispatch(
- updateQMC2Keys(
- objectify(
- qmcKeys,
- (item) => item.name,
- (item) => item.key
- )
- )
- );
+ const qmc2Keys = useSelector(selectStagingQMCv2Settings).keys;
- toast({
- title: 'QMCv2 密钥的更改已保存。',
- status: 'success',
- isClosable: true,
- duration: 2500,
- });
- };
- const clearAll = () => setQMCKeys([]);
- useEffect(resetQmcKeys, [qmcSettings.keys]);
+ const addKey = () => dispatch(qmc2AddKey());
+ const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent) =>
+ dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value }));
+ const deleteKey = (id: string) => dispatch(qmc2DeleteKey({ id }));
+ const clearAll = () => dispatch(qmc2ClearKeys());
return (
-
+
密钥
-
+
- }>
+ }>
添加
);
}
diff --git a/src/features/settings/persistSettings.ts b/src/features/settings/persistSettings.ts
index 420266b..5a1f6a5 100644
--- a/src/features/settings/persistSettings.ts
+++ b/src/features/settings/persistSettings.ts
@@ -2,14 +2,14 @@ import { debounce } from 'radash';
import { produce } from 'immer';
import type { AppStore } from '~/store';
-import { SettingsState, settingsSlice, updateSettings } from './settingsSlice';
+import { settingsSlice, setProductionChanges, ProductionSettings } 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) => {
+function mergeSettings(settings: ProductionSettings): ProductionSettings {
+ return produce(settingsSlice.getInitialState().production, (draft) => {
for (const [k, v] of enumObject(settings.qmc2?.keys)) {
if (typeof v === 'string') {
draft.qmc2.keys[k] = v;
@@ -22,10 +22,10 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE
let lastSettings: unknown;
try {
- const loadedSettings: SettingsState = JSON.parse(localStorage.getItem(storageKey) ?? '');
+ const loadedSettings: ProductionSettings = JSON.parse(localStorage.getItem(storageKey) ?? '');
if (loadedSettings) {
const mergedSettings = mergeSettings(loadedSettings);
- store.dispatch(updateSettings(mergedSettings));
+ store.dispatch(setProductionChanges(mergedSettings));
getLogger().debug('settings loaded');
}
} catch {
@@ -34,7 +34,7 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE
return store.subscribe(
debounce({ delay: 150 }, () => {
- const currentSettings = store.getState().settings;
+ const currentSettings = store.getState().settings.production;
if (lastSettings !== currentSettings) {
lastSettings = currentSettings;
localStorage.setItem(storageKey, JSON.stringify(currentSettings));
diff --git a/src/features/settings/settingsSelector.ts b/src/features/settings/settingsSelector.ts
index 300d09c..e837718 100644
--- a/src/features/settings/settingsSelector.ts
+++ b/src/features/settings/settingsSelector.ts
@@ -2,8 +2,11 @@ import type { DecryptCommandOptions } from '~/decrypt-worker/types';
import type { RootState } from '~/store';
import { hasOwn } from '~/util/objects';
+export const selectStagingQMCv2Settings = (state: RootState) => state.settings.staging.qmc2;
+export const selectFinalQMCv2Settings = (state: RootState) => state.settings.production.qmc2;
+
export const selectDecryptOptionByFile = (state: RootState, name: string): DecryptCommandOptions => {
- const qmc2Keys = state.settings.qmc2.keys;
+ const qmc2Keys = selectFinalQMCv2Settings(state).keys;
return {
qmc2Key: hasOwn(qmc2Keys, name) ? qmc2Keys[name] : undefined,
diff --git a/src/features/settings/settingsSlice.ts b/src/features/settings/settingsSlice.ts
index 2891840..0b55942 100644
--- a/src/features/settings/settingsSlice.ts
+++ b/src/features/settings/settingsSlice.ts
@@ -1,28 +1,92 @@
import { createSlice } from '@reduxjs/toolkit';
import type { PayloadAction } from '@reduxjs/toolkit';
-import type { RootState } from '~/store';
+import { nanoid } from 'nanoid';
+import { objectify } from 'radash';
-export interface QMCSettings {
- keys: Record; // { [fileName]: ekey }
+export interface StagingSettings {
+ qmc2: {
+ keys: { id: string; name: string; key: string }[];
+ };
+}
+
+export interface ProductionSettings {
+ qmc2: {
+ keys: Record; // { [fileName]: ekey }
+ };
}
export interface SettingsState {
- qmc2: QMCSettings;
+ staging: StagingSettings;
+ production: ProductionSettings;
}
-
const initialState: SettingsState = {
- qmc2: { keys: {} },
+ staging: {
+ qmc2: {
+ keys: [],
+ },
+ },
+ production: {
+ qmc2: {
+ keys: {},
+ },
+ },
};
+const stagingToProduction = (staging: StagingSettings): ProductionSettings => ({
+ qmc2: {
+ keys: objectify(
+ staging.qmc2.keys,
+ (item) => item.name,
+ (item) => item.key
+ ),
+ },
+});
+
+const productionToStaging = (production: ProductionSettings): StagingSettings => ({
+ qmc2: {
+ keys: Object.entries(production.qmc2.keys).map(([name, key]) => ({ id: nanoid(), name, key })),
+ },
+});
+
export const settingsSlice = createSlice({
name: 'settings',
initialState,
reducers: {
- updateSettings: (_state, { payload }: PayloadAction) => {
- return payload;
+ setProductionChanges: (_state, { payload }: PayloadAction) => {
+ return {
+ production: payload,
+ staging: productionToStaging(payload),
+ };
},
- updateQMC2Keys: (state, { payload }: PayloadAction) => {
- state.qmc2.keys = payload;
+ qmc2AddKey(state) {
+ state.staging.qmc2.keys.push({ id: nanoid(), name: '', key: '' });
+ },
+ qmc2DeleteKey(state, { payload: { id } }: PayloadAction<{ id: string }>) {
+ const qmc2 = state.staging.qmc2;
+ qmc2.keys = qmc2.keys.filter((item) => item.id !== id);
+ },
+ qmc2UpdateKey(
+ state,
+ { payload: { id, field, value } }: PayloadAction<{ id: string; field: 'name' | 'key'; value: string }>
+ ) {
+ const keyItem = state.staging.qmc2.keys.find((item) => item.id === id);
+ if (keyItem) {
+ keyItem[field] = value;
+ }
+ },
+ qmc2ClearKeys(state) {
+ state.staging.qmc2.keys = [];
+ },
+ discardStagingChanges: (state) => {
+ state.staging = productionToStaging(state.production);
+ },
+ commitStagingChange: (state) => {
+ const production = stagingToProduction(state.staging);
+ return {
+ // Sync back to staging
+ staging: productionToStaging(production),
+ production,
+ };
},
resetConfig: () => {
return initialState;
@@ -30,8 +94,17 @@ export const settingsSlice = createSlice({
},
});
-export const { updateSettings, resetConfig, updateQMC2Keys } = settingsSlice.actions;
+export const {
+ setProductionChanges,
+ resetConfig,
-export const selectQM2CSettings = (state: RootState) => state.settings.qmc2;
+ qmc2AddKey,
+ qmc2UpdateKey,
+ qmc2DeleteKey,
+ qmc2ClearKeys,
+
+ commitStagingChange,
+ discardStagingChanges,
+} = settingsSlice.actions;
export default settingsSlice.reducer;