feat: responsive settings ui
This commit is contained in:
parent
1f87a655ac
commit
725f130e42
@ -1,20 +1,95 @@
|
|||||||
import { Tab, TabList, TabPanel, TabPanels, Tabs, Text } from '@chakra-ui/react';
|
import {
|
||||||
import { PanelQMC } from './panels/PanelQMC';
|
Button,
|
||||||
|
Flex,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
MenuItem,
|
||||||
|
MenuList,
|
||||||
|
Portal,
|
||||||
|
Tab,
|
||||||
|
TabList,
|
||||||
|
TabPanel,
|
||||||
|
TabPanels,
|
||||||
|
Tabs,
|
||||||
|
Text,
|
||||||
|
useBreakpointValue,
|
||||||
|
} from '@chakra-ui/react';
|
||||||
|
import { PanelQMCv2Key } from './panels/PanelQMCv2Key';
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { MdExpandMore, MdMenu } from 'react-icons/md';
|
||||||
|
|
||||||
|
const TABS: { name: string; Tab: () => JSX.Element }[] = [
|
||||||
|
{ name: 'QMCv2 密钥', Tab: PanelQMCv2Key },
|
||||||
|
{
|
||||||
|
name: '其它/待定',
|
||||||
|
Tab: () => (
|
||||||
|
<TabPanel>
|
||||||
|
<Text>这里空空如也~</Text>
|
||||||
|
</TabPanel>
|
||||||
|
),
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
export function Settings() {
|
export function Settings() {
|
||||||
|
const isLargeWidthDevice =
|
||||||
|
useBreakpointValue({
|
||||||
|
base: false,
|
||||||
|
lg: true,
|
||||||
|
}) ?? false;
|
||||||
|
|
||||||
|
const [tabIndex, setTabIndex] = useState(0);
|
||||||
|
const handleTabChange = (idx: number) => {
|
||||||
|
setTabIndex(idx);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Tabs orientation="vertical" align="start" variant="line-i" flex={1}>
|
<Flex flexDir="column" flex={1}>
|
||||||
<TabList minW={0} width="8em" textAlign="right" justifyContent="center">
|
<Menu>
|
||||||
<Tab>QQ 音乐</Tab>
|
<MenuButton
|
||||||
<Tab>其它</Tab>
|
as={Button}
|
||||||
|
leftIcon={<MdMenu />}
|
||||||
|
rightIcon={<MdExpandMore />}
|
||||||
|
colorScheme="gray"
|
||||||
|
variant="outline"
|
||||||
|
w="full"
|
||||||
|
flexShrink={0}
|
||||||
|
hidden={isLargeWidthDevice}
|
||||||
|
mb="4"
|
||||||
|
>
|
||||||
|
{TABS[tabIndex].name}
|
||||||
|
</MenuButton>
|
||||||
|
<Portal>
|
||||||
|
<MenuList w="100px">
|
||||||
|
{TABS.map(({ name }, i) => (
|
||||||
|
<MenuItem key={name} onClick={() => setTabIndex(i)}>
|
||||||
|
{name}
|
||||||
|
</MenuItem>
|
||||||
|
))}
|
||||||
|
</MenuList>
|
||||||
|
</Portal>
|
||||||
|
</Menu>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
orientation={isLargeWidthDevice ? 'vertical' : 'horizontal'}
|
||||||
|
align="start"
|
||||||
|
variant="line-i"
|
||||||
|
display="flex"
|
||||||
|
flex={1}
|
||||||
|
index={tabIndex}
|
||||||
|
onChange={handleTabChange}
|
||||||
|
>
|
||||||
|
<TabList hidden={!isLargeWidthDevice} minW="8em" width="8em" textAlign="right" justifyContent="center">
|
||||||
|
{TABS.map(({ name }) => (
|
||||||
|
<Tab key={name}>{name}</Tab>
|
||||||
|
))}
|
||||||
</TabList>
|
</TabList>
|
||||||
|
|
||||||
<TabPanels>
|
<TabPanels>
|
||||||
<PanelQMC />
|
{TABS.map(({ name, Tab }) => (
|
||||||
<TabPanel>
|
<Tab key={name} />
|
||||||
<Text>待定</Text>
|
))}
|
||||||
</TabPanel>
|
|
||||||
</TabPanels>
|
</TabPanels>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
</Flex>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,153 +0,0 @@
|
|||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
ButtonGroup,
|
|
||||||
Flex,
|
|
||||||
HStack,
|
|
||||||
Heading,
|
|
||||||
Icon,
|
|
||||||
IconButton,
|
|
||||||
Input,
|
|
||||||
InputGroup,
|
|
||||||
InputLeftElement,
|
|
||||||
InputRightElement,
|
|
||||||
List,
|
|
||||||
ListItem,
|
|
||||||
Menu,
|
|
||||||
MenuButton,
|
|
||||||
MenuDivider,
|
|
||||||
MenuItem,
|
|
||||||
MenuList,
|
|
||||||
Spacer,
|
|
||||||
TabPanel,
|
|
||||||
Text,
|
|
||||||
VStack,
|
|
||||||
} from '@chakra-ui/react';
|
|
||||||
import { useSelector } from 'react-redux';
|
|
||||||
import { selectQM2CSettings } from '../settingsSlice';
|
|
||||||
import React, { useEffect, useState } from 'react';
|
|
||||||
import { nanoid } from 'nanoid';
|
|
||||||
import { produce } from 'immer';
|
|
||||||
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload, MdVpnKey } from 'react-icons/md';
|
|
||||||
|
|
||||||
interface InternalQMCKeys {
|
|
||||||
id: string;
|
|
||||||
name: string;
|
|
||||||
key: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PanelQMC() {
|
|
||||||
const qmcSettings = useSelector(selectQM2CSettings);
|
|
||||||
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 = () => {
|
|
||||||
setQMCKeys((prev) => [...prev, { id: nanoid(), key: '', name: '' }]);
|
|
||||||
};
|
|
||||||
const updateKey = (prop: 'name' | 'key', id: string, e: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setQMCKeys((prev) =>
|
|
||||||
produce(prev, (draft) => {
|
|
||||||
const item = draft.find((item) => item.id === id);
|
|
||||||
if (item) {
|
|
||||||
item[prop] = e.target.value;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
);
|
|
||||||
};
|
|
||||||
const applyChanges = () => {
|
|
||||||
//
|
|
||||||
};
|
|
||||||
const clearAll = () => setQMCKeys([]);
|
|
||||||
useEffect(resetQmcKeys, [qmcSettings.keys]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Flex as={TabPanel} flexDir="column" h="100%">
|
|
||||||
<Box flex={1} minH={0} overflow="auto">
|
|
||||||
<Heading as="h2" size="lg">
|
|
||||||
密钥
|
|
||||||
</Heading>
|
|
||||||
|
|
||||||
<Box p="4" pr="0" borderStart="2px solid" borderColor="gray.200">
|
|
||||||
<List spacing={3}>
|
|
||||||
{qmcKeys.map(({ id, key, name }, i) => (
|
|
||||||
<ListItem key={id}>
|
|
||||||
<HStack>
|
|
||||||
<Text w="2em" textAlign="center">
|
|
||||||
{i + 1}
|
|
||||||
</Text>
|
|
||||||
|
|
||||||
<VStack flex={1}>
|
|
||||||
<Input
|
|
||||||
variant="flushed"
|
|
||||||
placeholder="文件名"
|
|
||||||
value={name}
|
|
||||||
onChange={(e) => updateKey('name', id, e)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InputGroup size="xs">
|
|
||||||
<InputLeftElement pr="2">
|
|
||||||
<Icon as={MdVpnKey} />
|
|
||||||
</InputLeftElement>
|
|
||||||
<Input
|
|
||||||
variant="flushed"
|
|
||||||
placeholder="密钥"
|
|
||||||
value={key}
|
|
||||||
onChange={(e) => updateKey('key', id, e)}
|
|
||||||
/>
|
|
||||||
<InputRightElement>
|
|
||||||
<Text pl="2" color={key.length ? 'green.500' : 'red.500'}>
|
|
||||||
<code>{key.length || '?'}</code>
|
|
||||||
</Text>
|
|
||||||
</InputRightElement>
|
|
||||||
</InputGroup>
|
|
||||||
</VStack>
|
|
||||||
</HStack>
|
|
||||||
</ListItem>
|
|
||||||
))}
|
|
||||||
</List>
|
|
||||||
{qmcKeys.length === 0 && <Text>还没有添加密钥。</Text>}
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
|
|
||||||
<VStack mt="4" alignItems="flex-start">
|
|
||||||
<Text>密钥填充完毕后,按下「应用」来使用新的设置。</Text>
|
|
||||||
<Flex flexDir="row" gap="2" w="full">
|
|
||||||
<Box>
|
|
||||||
<ButtonGroup isAttached variant="outline">
|
|
||||||
<Button onClick={addRow}>
|
|
||||||
<Icon as={MdAdd} />
|
|
||||||
添加密钥
|
|
||||||
</Button>
|
|
||||||
<Menu>
|
|
||||||
<MenuButton as={IconButton} icon={<MdExpandMore />}>
|
|
||||||
<Icon as={MdExpandMore} />1
|
|
||||||
</MenuButton>
|
|
||||||
<MenuList>
|
|
||||||
<MenuItem onClick={() => alert('TODO!')} icon={<Icon as={MdFileUpload} h={18} w={18} />}>
|
|
||||||
从文件导入 (JSON)
|
|
||||||
</MenuItem>
|
|
||||||
<MenuDivider />
|
|
||||||
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} h={18} w={18} />}>
|
|
||||||
清空
|
|
||||||
</MenuItem>
|
|
||||||
</MenuList>
|
|
||||||
</Menu>
|
|
||||||
</ButtonGroup>
|
|
||||||
</Box>
|
|
||||||
<Spacer />
|
|
||||||
<Box>
|
|
||||||
<Button onClick={resetQmcKeys} colorScheme="red" variant="ghost">
|
|
||||||
放弃
|
|
||||||
</Button>
|
|
||||||
<Button onClick={applyChanges}>应用</Button>
|
|
||||||
</Box>
|
|
||||||
</Flex>
|
|
||||||
</VStack>
|
|
||||||
</Flex>
|
|
||||||
);
|
|
||||||
}
|
|
183
src/features/settings/panels/PanelQMCv2Key.tsx
Normal file
183
src/features/settings/panels/PanelQMCv2Key.tsx
Normal file
@ -0,0 +1,183 @@
|
|||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
ButtonGroup,
|
||||||
|
Center,
|
||||||
|
Flex,
|
||||||
|
HStack,
|
||||||
|
Heading,
|
||||||
|
Icon,
|
||||||
|
IconButton,
|
||||||
|
Input,
|
||||||
|
InputGroup,
|
||||||
|
InputLeftElement,
|
||||||
|
InputRightElement,
|
||||||
|
List,
|
||||||
|
ListItem,
|
||||||
|
Menu,
|
||||||
|
MenuButton,
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
toast({
|
||||||
|
title: 'QMCv2 密钥的更改已保存。',
|
||||||
|
status: 'success',
|
||||||
|
isClosable: true,
|
||||||
|
duration: 2500,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
const clearAll = () => setQMCKeys([]);
|
||||||
|
useEffect(resetQmcKeys, [qmcSettings.keys]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Flex as={TabPanel} flexDir="column" h="100%">
|
||||||
|
<Heading as="h2" size="lg">
|
||||||
|
密钥
|
||||||
|
</Heading>
|
||||||
|
|
||||||
|
<Box pb="2">
|
||||||
|
<ButtonGroup isAttached variant="outline">
|
||||||
|
<Button onClick={addRow} leftIcon={<Icon as={MdAdd} />}>
|
||||||
|
添加
|
||||||
|
</Button>
|
||||||
|
<Menu>
|
||||||
|
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
|
||||||
|
<MenuList>
|
||||||
|
<MenuItem onClick={() => alert('TODO!')} icon={<Icon as={MdFileUpload} boxSize={5} />}>
|
||||||
|
从文件导入(JSON)
|
||||||
|
</MenuItem>
|
||||||
|
{/* 需要加入 SQL.js 再处理 */}
|
||||||
|
<MenuItem hidden onClick={() => alert('TODO!')} icon={<Icon as={MdAndroid} boxSize={5} />}>
|
||||||
|
从文件导入(安卓数据库)
|
||||||
|
</MenuItem>
|
||||||
|
<MenuDivider />
|
||||||
|
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
|
||||||
|
清空
|
||||||
|
</MenuItem>
|
||||||
|
</MenuList>
|
||||||
|
</Menu>
|
||||||
|
</ButtonGroup>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box flex={1} minH={0} overflow="auto" pr="4">
|
||||||
|
<List spacing={3}>
|
||||||
|
{qmcKeys.map(({ id, key, name }, i) => (
|
||||||
|
<ListItem key={id}>
|
||||||
|
<HStack>
|
||||||
|
<Text w="2em" textAlign="center">
|
||||||
|
{i + 1}
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
<VStack flex={1}>
|
||||||
|
<Input
|
||||||
|
variant="flushed"
|
||||||
|
placeholder="文件名"
|
||||||
|
value={name}
|
||||||
|
onChange={(e) => updateKey('name', id, e)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<InputGroup size="xs">
|
||||||
|
<InputLeftElement pr="2">
|
||||||
|
<Icon as={MdVpnKey} />
|
||||||
|
</InputLeftElement>
|
||||||
|
<Input variant="flushed" placeholder="密钥" value={key} onChange={(e) => updateKey('key', id, e)} />
|
||||||
|
<InputRightElement>
|
||||||
|
<Text pl="2" color={key.length ? 'green.500' : 'red.500'}>
|
||||||
|
<code>{key.length || '?'}</code>
|
||||||
|
</Text>
|
||||||
|
</InputRightElement>
|
||||||
|
</InputGroup>
|
||||||
|
</VStack>
|
||||||
|
</HStack>
|
||||||
|
</ListItem>
|
||||||
|
))}
|
||||||
|
</List>
|
||||||
|
{qmcKeys.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>
|
||||||
|
);
|
||||||
|
}
|
@ -21,13 +21,16 @@ export const settingsSlice = createSlice({
|
|||||||
updateSettings: (_state, { payload }: PayloadAction<SettingsState>) => {
|
updateSettings: (_state, { payload }: PayloadAction<SettingsState>) => {
|
||||||
return payload;
|
return payload;
|
||||||
},
|
},
|
||||||
|
updateQMC2Keys: (state, { payload }: PayloadAction<QMCSettings['keys']>) => {
|
||||||
|
state.qmc2.keys = payload;
|
||||||
|
},
|
||||||
resetConfig: () => {
|
resetConfig: () => {
|
||||||
return initialState;
|
return initialState;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
export const { updateSettings, resetConfig } = settingsSlice.actions;
|
export const { updateSettings, resetConfig, updateQMC2Keys } = settingsSlice.actions;
|
||||||
|
|
||||||
export const selectQM2CSettings = (state: RootState) => state.settings.qmc2;
|
export const selectQM2CSettings = (state: RootState) => state.settings.qmc2;
|
||||||
|
|
||||||
|
@ -1,9 +1,14 @@
|
|||||||
import { Container, Flex } from '@chakra-ui/react';
|
import { Container, Flex, useBreakpointValue } from '@chakra-ui/react';
|
||||||
import { Settings } from '~/features/settings/Settings';
|
import { Settings } from '~/features/settings/Settings';
|
||||||
|
|
||||||
export function SettingsTab() {
|
export function SettingsTab() {
|
||||||
|
const containerProps = useBreakpointValue({
|
||||||
|
base: { p: '0' },
|
||||||
|
lg: { p: undefined },
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Container as={Flex} maxW="container.lg">
|
<Container as={Flex} maxW="container.lg" {...containerProps}>
|
||||||
<Settings />
|
<Settings />
|
||||||
</Container>
|
</Container>
|
||||||
);
|
);
|
||||||
|
@ -17,6 +17,13 @@ const variantLineInvert = definePartsStyle((props) => {
|
|||||||
[borderProp]: '2px solid',
|
[borderProp]: '2px solid',
|
||||||
borderColor: 'inherit',
|
borderColor: 'inherit',
|
||||||
},
|
},
|
||||||
|
tabpanels: {
|
||||||
|
flex: 1,
|
||||||
|
minH: 0,
|
||||||
|
},
|
||||||
|
tabpanel: {
|
||||||
|
padding: 0,
|
||||||
|
},
|
||||||
tab: {
|
tab: {
|
||||||
[borderProp]: '2px solid',
|
[borderProp]: '2px solid',
|
||||||
borderColor: 'transparent',
|
borderColor: 'transparent',
|
||||||
@ -42,7 +49,10 @@ const variantLineInvert = definePartsStyle((props) => {
|
|||||||
bg: $bg.reference,
|
bg: $bg.reference,
|
||||||
},
|
},
|
||||||
root: {
|
root: {
|
||||||
|
display: 'flex',
|
||||||
|
flexDir: isVertical ? 'row' : 'column',
|
||||||
gap: 8,
|
gap: 8,
|
||||||
|
minH: 0,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
Loading…
Reference in New Issue
Block a user