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 ( - + 密钥 - + - @@ -120,8 +69,8 @@ export function PanelQMCv2Key() { - {qmcKeys.map(({ id, key, name }, i) => ( - + {qmc2Keys.map(({ id, key, name }, i) => ( + {i + 1} @@ -147,37 +96,21 @@ export function PanelQMCv2Key() { + + } + variant="ghost" + colorScheme="red" + type="button" + onClick={() => deleteKey(id)} + /> ))} - {qmcKeys.length === 0 && 还没有添加密钥。} + {qmc2Keys.length === 0 && 还没有添加密钥。} - - - -
- - 重复项只保留最后一项。 - -
- - - - - -
-
); } 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;