mirror of
https://git.unlock-music.dev/um/um-react.git
synced 2024-11-27 17:42:16 +00:00
feat: split settings slice to staging (ui) and production (in effect)
This commit is contained in:
parent
865dcae931
commit
f46f36415d
@ -1,36 +1,40 @@
|
|||||||
import {
|
import {
|
||||||
|
Box,
|
||||||
Button,
|
Button,
|
||||||
|
Center,
|
||||||
Flex,
|
Flex,
|
||||||
|
HStack,
|
||||||
Menu,
|
Menu,
|
||||||
MenuButton,
|
MenuButton,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
Portal,
|
Portal,
|
||||||
|
Spacer,
|
||||||
Tab,
|
Tab,
|
||||||
TabList,
|
TabList,
|
||||||
TabPanel,
|
TabPanel,
|
||||||
TabPanels,
|
TabPanels,
|
||||||
Tabs,
|
Tabs,
|
||||||
Text,
|
Text,
|
||||||
|
VStack,
|
||||||
useBreakpointValue,
|
useBreakpointValue,
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
|
import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
import { MdExpandMore, MdMenu } from 'react-icons/md';
|
import { MdExpandMore, MdMenu } from 'react-icons/md';
|
||||||
|
import { useAppDispatch } from '~/hooks';
|
||||||
|
import { commitStagingChange, discardStagingChanges } from './settingsSlice';
|
||||||
|
|
||||||
const TABS: { name: string; Tab: () => JSX.Element }[] = [
|
const TABS: { name: string; Tab: () => JSX.Element }[] = [
|
||||||
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
|
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
|
||||||
{
|
{
|
||||||
name: '其它/待定',
|
name: '其它/待定',
|
||||||
Tab: () => (
|
Tab: () => <Text>这里空空如也~</Text>,
|
||||||
<TabPanel>
|
|
||||||
<Text>这里空空如也~</Text>
|
|
||||||
</TabPanel>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
|
const dispatch = useAppDispatch();
|
||||||
const isLargeWidthDevice =
|
const isLargeWidthDevice =
|
||||||
useBreakpointValue({
|
useBreakpointValue({
|
||||||
base: false,
|
base: false,
|
||||||
@ -41,6 +45,8 @@ export function Settings() {
|
|||||||
const handleTabChange = (idx: number) => {
|
const handleTabChange = (idx: number) => {
|
||||||
setTabIndex(idx);
|
setTabIndex(idx);
|
||||||
};
|
};
|
||||||
|
const handleResetSettings = () => dispatch(discardStagingChanges());
|
||||||
|
const handleApplySettings = () => dispatch(commitStagingChange());
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex flexDir="column" flex={1}>
|
<Flex flexDir="column" flex={1}>
|
||||||
@ -86,7 +92,26 @@ export function Settings() {
|
|||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
{TABS.map(({ name, Tab }) => (
|
{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>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
@ -2,7 +2,6 @@ import {
|
|||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
ButtonGroup,
|
ButtonGroup,
|
||||||
Center,
|
|
||||||
Flex,
|
Flex,
|
||||||
HStack,
|
HStack,
|
||||||
Heading,
|
Heading,
|
||||||
@ -19,84 +18,34 @@ import {
|
|||||||
MenuDivider,
|
MenuDivider,
|
||||||
MenuItem,
|
MenuItem,
|
||||||
MenuList,
|
MenuList,
|
||||||
Spacer,
|
|
||||||
TabPanel,
|
|
||||||
Text,
|
Text,
|
||||||
VStack,
|
VStack,
|
||||||
useToast,
|
|
||||||
} from '@chakra-ui/react';
|
} from '@chakra-ui/react';
|
||||||
import { useDispatch, useSelector } from 'react-redux';
|
import { useDispatch, useSelector } from 'react-redux';
|
||||||
import { selectQM2CSettings, updateQMC2Keys } from '../settingsSlice';
|
import { qmc2AddKey, qmc2ClearKeys, qmc2DeleteKey, qmc2UpdateKey } from '../settingsSlice';
|
||||||
import React, { useEffect, useState } from 'react';
|
import { selectStagingQMCv2Settings } from '../settingsSelector';
|
||||||
import { nanoid } from 'nanoid';
|
import React from 'react';
|
||||||
import { produce } from 'immer';
|
import { MdAdd, MdAndroid, MdDelete, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
|
||||||
import { MdAdd, MdAndroid, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
|
|
||||||
import { objectify } from 'radash';
|
|
||||||
|
|
||||||
interface InternalQMCKeys {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PanelQMCv2Key() {
|
export function PanelQMCv2Key() {
|
||||||
const toast = useToast();
|
|
||||||
const dispatch = useDispatch();
|
const dispatch = useDispatch();
|
||||||
const qmcSettings = useSelector(selectQM2CSettings);
|
const qmc2Keys = useSelector(selectStagingQMCv2Settings).keys;
|
||||||
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
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
toast({
|
const addKey = () => dispatch(qmc2AddKey());
|
||||||
title: 'QMCv2 密钥的更改已保存。',
|
const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent<HTMLInputElement>) =>
|
||||||
status: 'success',
|
dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value }));
|
||||||
isClosable: true,
|
const deleteKey = (id: string) => dispatch(qmc2DeleteKey({ id }));
|
||||||
duration: 2500,
|
const clearAll = () => dispatch(qmc2ClearKeys());
|
||||||
});
|
|
||||||
};
|
|
||||||
const clearAll = () => setQMCKeys([]);
|
|
||||||
useEffect(resetQmcKeys, [qmcSettings.keys]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Flex as={TabPanel} flexDir="column" h="100%">
|
<Flex minH={0} flexDir="column" flex={1}>
|
||||||
<Heading as="h2" size="lg">
|
<Heading as="h2" size="lg">
|
||||||
密钥
|
密钥
|
||||||
</Heading>
|
</Heading>
|
||||||
|
|
||||||
<Box pb="2">
|
<Box pb={2} pt={2}>
|
||||||
<ButtonGroup isAttached variant="outline">
|
<ButtonGroup isAttached variant="outline">
|
||||||
<Button onClick={addRow} leftIcon={<Icon as={MdAdd} />}>
|
<Button onClick={addKey} leftIcon={<Icon as={MdAdd} />}>
|
||||||
添加
|
添加
|
||||||
</Button>
|
</Button>
|
||||||
<Menu>
|
<Menu>
|
||||||
@ -120,8 +69,8 @@ export function PanelQMCv2Key() {
|
|||||||
|
|
||||||
<Box flex={1} minH={0} overflow="auto" pr="4">
|
<Box flex={1} minH={0} overflow="auto" pr="4">
|
||||||
<List spacing={3}>
|
<List spacing={3}>
|
||||||
{qmcKeys.map(({ id, key, name }, i) => (
|
{qmc2Keys.map(({ id, key, name }, i) => (
|
||||||
<ListItem key={id}>
|
<ListItem key={id} mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}>
|
||||||
<HStack>
|
<HStack>
|
||||||
<Text w="2em" textAlign="center">
|
<Text w="2em" textAlign="center">
|
||||||
{i + 1}
|
{i + 1}
|
||||||
@ -147,37 +96,21 @@ export function PanelQMCv2Key() {
|
|||||||
</InputRightElement>
|
</InputRightElement>
|
||||||
</InputGroup>
|
</InputGroup>
|
||||||
</VStack>
|
</VStack>
|
||||||
|
|
||||||
|
<IconButton
|
||||||
|
aria-label="删除该密钥"
|
||||||
|
icon={<Icon as={MdDelete} boxSize={6} />}
|
||||||
|
variant="ghost"
|
||||||
|
colorScheme="red"
|
||||||
|
type="button"
|
||||||
|
onClick={() => deleteKey(id)}
|
||||||
|
/>
|
||||||
</HStack>
|
</HStack>
|
||||||
</ListItem>
|
</ListItem>
|
||||||
))}
|
))}
|
||||||
</List>
|
</List>
|
||||||
{qmcKeys.length === 0 && <Text>还没有添加密钥。</Text>}
|
{qmc2Keys.length === 0 && <Text>还没有添加密钥。</Text>}
|
||||||
</Box>
|
</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>
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -2,14 +2,14 @@ import { debounce } from 'radash';
|
|||||||
import { produce } from 'immer';
|
import { produce } from 'immer';
|
||||||
|
|
||||||
import type { AppStore } from '~/store';
|
import type { AppStore } from '~/store';
|
||||||
import { SettingsState, settingsSlice, updateSettings } from './settingsSlice';
|
import { settingsSlice, setProductionChanges, ProductionSettings } from './settingsSlice';
|
||||||
import { enumObject } from '~/util/objects';
|
import { enumObject } from '~/util/objects';
|
||||||
import { getLogger } from '~/util/logUtils';
|
import { getLogger } from '~/util/logUtils';
|
||||||
|
|
||||||
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
const DEFAULT_STORAGE_KEY = 'um-react-settings';
|
||||||
|
|
||||||
function mergeSettings(settings: SettingsState): SettingsState {
|
function mergeSettings(settings: ProductionSettings): ProductionSettings {
|
||||||
return produce(settingsSlice.getInitialState(), (draft) => {
|
return produce(settingsSlice.getInitialState().production, (draft) => {
|
||||||
for (const [k, v] of enumObject(settings.qmc2?.keys)) {
|
for (const [k, v] of enumObject(settings.qmc2?.keys)) {
|
||||||
if (typeof v === 'string') {
|
if (typeof v === 'string') {
|
||||||
draft.qmc2.keys[k] = v;
|
draft.qmc2.keys[k] = v;
|
||||||
@ -22,10 +22,10 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE
|
|||||||
let lastSettings: unknown;
|
let lastSettings: unknown;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const loadedSettings: SettingsState = JSON.parse(localStorage.getItem(storageKey) ?? '');
|
const loadedSettings: ProductionSettings = JSON.parse(localStorage.getItem(storageKey) ?? '');
|
||||||
if (loadedSettings) {
|
if (loadedSettings) {
|
||||||
const mergedSettings = mergeSettings(loadedSettings);
|
const mergedSettings = mergeSettings(loadedSettings);
|
||||||
store.dispatch(updateSettings(mergedSettings));
|
store.dispatch(setProductionChanges(mergedSettings));
|
||||||
getLogger().debug('settings loaded');
|
getLogger().debug('settings loaded');
|
||||||
}
|
}
|
||||||
} catch {
|
} catch {
|
||||||
@ -34,7 +34,7 @@ export function persistSettings(store: AppStore, storageKey = DEFAULT_STORAGE_KE
|
|||||||
|
|
||||||
return store.subscribe(
|
return store.subscribe(
|
||||||
debounce({ delay: 150 }, () => {
|
debounce({ delay: 150 }, () => {
|
||||||
const currentSettings = store.getState().settings;
|
const currentSettings = store.getState().settings.production;
|
||||||
if (lastSettings !== currentSettings) {
|
if (lastSettings !== currentSettings) {
|
||||||
lastSettings = currentSettings;
|
lastSettings = currentSettings;
|
||||||
localStorage.setItem(storageKey, JSON.stringify(currentSettings));
|
localStorage.setItem(storageKey, JSON.stringify(currentSettings));
|
||||||
|
@ -2,8 +2,11 @@ import type { DecryptCommandOptions } from '~/decrypt-worker/types';
|
|||||||
import type { RootState } from '~/store';
|
import type { RootState } from '~/store';
|
||||||
import { hasOwn } from '~/util/objects';
|
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 => {
|
export const selectDecryptOptionByFile = (state: RootState, name: string): DecryptCommandOptions => {
|
||||||
const qmc2Keys = state.settings.qmc2.keys;
|
const qmc2Keys = selectFinalQMCv2Settings(state).keys;
|
||||||
|
|
||||||
return {
|
return {
|
||||||
qmc2Key: hasOwn(qmc2Keys, name) ? qmc2Keys[name] : undefined,
|
qmc2Key: hasOwn(qmc2Keys, name) ? qmc2Keys[name] : undefined,
|
||||||
|
@ -1,28 +1,92 @@
|
|||||||
import { createSlice } from '@reduxjs/toolkit';
|
import { createSlice } from '@reduxjs/toolkit';
|
||||||
import type { PayloadAction } 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 {
|
export interface StagingSettings {
|
||||||
keys: Record<string, string>; // { [fileName]: ekey }
|
qmc2: {
|
||||||
|
keys: { id: string; name: string; key: string }[];
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ProductionSettings {
|
||||||
|
qmc2: {
|
||||||
|
keys: Record<string, string>; // { [fileName]: ekey }
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SettingsState {
|
export interface SettingsState {
|
||||||
qmc2: QMCSettings;
|
staging: StagingSettings;
|
||||||
|
production: ProductionSettings;
|
||||||
}
|
}
|
||||||
|
|
||||||
const initialState: SettingsState = {
|
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({
|
export const settingsSlice = createSlice({
|
||||||
name: 'settings',
|
name: 'settings',
|
||||||
initialState,
|
initialState,
|
||||||
reducers: {
|
reducers: {
|
||||||
updateSettings: (_state, { payload }: PayloadAction<SettingsState>) => {
|
setProductionChanges: (_state, { payload }: PayloadAction<ProductionSettings>) => {
|
||||||
return payload;
|
return {
|
||||||
|
production: payload,
|
||||||
|
staging: productionToStaging(payload),
|
||||||
|
};
|
||||||
},
|
},
|
||||||
updateQMC2Keys: (state, { payload }: PayloadAction<QMCSettings['keys']>) => {
|
qmc2AddKey(state) {
|
||||||
state.qmc2.keys = payload;
|
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: () => {
|
resetConfig: () => {
|
||||||
return initialState;
|
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;
|
export default settingsSlice.reducer;
|
||||||
|
Loading…
Reference in New Issue
Block a user