添加设定界面 - #18 #21
@ -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: () => (
|
||||
<TabPanel>
|
||||
<Text>这里空空如也~</Text>
|
||||
</TabPanel>
|
||||
),
|
||||
Tab: () => <Text>这里空空如也~</Text>,
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<Flex flexDir="column" flex={1}>
|
||||
@ -86,7 +92,26 @@ export function Settings() {
|
||||
|
||||
<TabPanels>
|
||||
{TABS.map(({ name, Tab }) => (
|
||||
<Tab key={name} />
|
||||
<Flex as={TabPanel} flex={1} flexDir="column" h="100%" key={name}>
|
||||
<Flex h="100%" flex={1} minH={0}>
|
||||
<Tab />
|
||||
</Flex>
|
||||
|
||||
<VStack mt="4" alignItems="flex-start" w="full">
|
||||
<Flex flexDir="row" gap="2" w="full">
|
||||
<Center>
|
||||
<Box color="gray">设置会在保存后生效。</Box>
|
||||
</Center>
|
||||
<Spacer />
|
||||
<HStack gap="2" justifyContent="flex-end">
|
||||
<Button onClick={handleResetSettings} colorScheme="red" variant="ghost" title="重置为更改前的状态">
|
||||
重置
|
||||
</Button>
|
||||
<Button onClick={handleApplySettings}>应用</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</Flex>
|
||||
))}
|
||||
</TabPanels>
|
||||
</Tabs>
|
||||
|
@ -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<InternalQMCKeys[]>([]);
|
||||
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<HTMLInputElement>) => {
|
||||
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<HTMLInputElement>) =>
|
||||
dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value }));
|
||||
const deleteKey = (id: string) => dispatch(qmc2DeleteKey({ id }));
|
||||
const clearAll = () => dispatch(qmc2ClearKeys());
|
||||
|
||||
return (
|
||||
<Flex as={TabPanel} flexDir="column" h="100%">
|
||||
<Flex minH={0} flexDir="column" flex={1}>
|
||||
<Heading as="h2" size="lg">
|
||||
密钥
|
||||
</Heading>
|
||||
|
||||
<Box pb="2">
|
||||
<Box pb={2} pt={2}>
|
||||
<ButtonGroup isAttached variant="outline">
|
||||
<Button onClick={addRow} leftIcon={<Icon as={MdAdd} />}>
|
||||
<Button onClick={addKey} leftIcon={<Icon as={MdAdd} />}>
|
||||
添加
|
||||
</Button>
|
||||
<Menu>
|
||||
@ -120,8 +69,8 @@ export function PanelQMCv2Key() {
|
||||
|
||||
<Box flex={1} minH={0} overflow="auto" pr="4">
|
||||
<List spacing={3}>
|
||||
{qmcKeys.map(({ id, key, name }, i) => (
|
||||
<ListItem key={id}>
|
||||
{qmc2Keys.map(({ id, key, name }, i) => (
|
||||
<ListItem key={id} mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}>
|
||||
<HStack>
|
||||
<Text w="2em" textAlign="center">
|
||||
{i + 1}
|
||||
@ -147,37 +96,21 @@ export function PanelQMCv2Key() {
|
||||
</InputRightElement>
|
||||
</InputGroup>
|
||||
</VStack>
|
||||
|
||||
<IconButton
|
||||
aria-label="删除该密钥"
|
||||
icon={<Icon as={MdDelete} boxSize={6} />}
|
||||
variant="ghost"
|
||||
colorScheme="red"
|
||||
type="button"
|
||||
onClick={() => deleteKey(id)}
|
||||
/>
|
||||
</HStack>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
{qmcKeys.length === 0 && <Text>还没有添加密钥。</Text>}
|
||||
{qmc2Keys.length === 0 && <Text>还没有添加密钥。</Text>}
|
||||
</Box>
|
||||
|
||||
<VStack mt="4" alignItems="flex-start" w="full">
|
||||
<Flex flexDir="row" gap="2" w="full">
|
||||
<Center>
|
||||
<Text as={Box} color="gray">
|
||||
重复项只保留最后一项。
|
||||
</Text>
|
||||
</Center>
|
||||
<Spacer />
|
||||
<HStack gap="2" justifyContent="flex-end">
|
||||
<Button
|
||||
disabled={!isModified}
|
||||
onClick={resetQmcKeys}
|
||||
colorScheme="red"
|
||||
variant="ghost"
|
||||
title="重置为更改前的状态"
|
||||
>
|
||||
重置
|
||||
</Button>
|
||||
<Button disabled={!isModified} onClick={applyChanges}>
|
||||
应用
|
||||
</Button>
|
||||
</HStack>
|
||||
</Flex>
|
||||
</VStack>
|
||||
</Flex>
|
||||
);
|
||||
}
|
||||
|
@ -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));
|
||||
|
@ -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,
|
||||
|
@ -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<string, string>; // { [fileName]: ekey }
|
||||
export interface StagingSettings {
|
||||
qmc2: {
|
||||
keys: { id: string; name: string; key: string }[];
|
||||
};
|
||||
}
|
||||
|
||||
export interface ProductionSettings {
|
||||
qmc2: {
|
||||
keys: Record<string, string>; // { [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<SettingsState>) => {
|
||||
return payload;
|
||||
setProductionChanges: (_state, { payload }: PayloadAction<ProductionSettings>) => {
|
||||
return {
|
||||
production: payload,
|
||||
staging: productionToStaging(payload),
|
||||
};
|
||||
},
|
||||
updateQMC2Keys: (state, { payload }: PayloadAction<QMCSettings['keys']>) => {
|
||||
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;
|
||||
|
Loading…
Reference in New Issue
Block a user