feat: kwm v2 key import ui

This commit is contained in:
鲁树人
2023-06-17 02:45:31 +01:00
parent 5492628b71
commit b277000c2a
13 changed files with 480 additions and 32 deletions

View File

@@ -0,0 +1,76 @@
import {
HStack,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
ListItem,
Text,
VStack,
} from '@chakra-ui/react';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { kwm2DeleteKey, kwm2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
import { memo } from 'react';
import { StagingKWMv2Key } from '../../keyFormats';
export const KWMv2EKeyItem = memo(({ id, ekey, quality, rid, i }: StagingKWMv2Key & { i: number }) => {
const dispatch = useAppDispatch();
const updateKey = (prop: keyof StagingKWMv2Key, e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(kwm2UpdateKey({ id, field: prop, value: e.target.value }));
const deleteKey = () => dispatch(kwm2DeleteKey({ id }));
return (
<ListItem mt={0} pt={2} pb={2} _even={{ bg: 'gray.50' }}>
<HStack>
<Text w="2em" textAlign="center">
{i + 1}
</Text>
<VStack flex={1}>
<HStack flex={1} w="full">
<Input
variant="flushed"
placeholder="资源 ID"
value={rid}
onChange={(e) => updateKey('rid', e)}
type="number"
maxW="8em"
/>
<Input
variant="flushed"
placeholder="音质格式"
value={quality}
onChange={(e) => updateKey('quality', e)}
flex={1}
/>
</HStack>
<InputGroup size="xs">
<InputLeftElement pr="2">
<Icon as={MdVpnKey} />
</InputLeftElement>
<Input variant="flushed" placeholder="密钥" value={ekey} onChange={(e) => updateKey('ekey', e)} />
<InputRightElement>
<Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}>
<code>{ekey.length || '?'}</code>
</Text>
</InputRightElement>
</InputGroup>
</VStack>
<IconButton
aria-label="删除该密钥"
icon={<Icon as={MdDelete} boxSize={6} />}
variant="ghost"
colorScheme="red"
type="button"
onClick={deleteKey}
/>
</HStack>
</ListItem>
);
});

View File

@@ -0,0 +1,94 @@
import {
Box,
Button,
ButtonGroup,
Code,
Flex,
HStack,
Heading,
Icon,
IconButton,
List,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Text,
useToast,
} from '@chakra-ui/react';
import { useDispatch, useSelector } from 'react-redux';
import { kwm2AddKey, kwm2ClearKeys } from '../settingsSlice';
import { selectStagingKWMv2Keys } from '../settingsSelector';
import { useState } from 'react';
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
import { KWMv2EKeyItem } from './KWMv2/KWMv2EKeyItem';
import { ImportSecretModal } from '~/components/ImportSecretModal';
export function PanelKWMv2Key() {
const toast = useToast();
const dispatch = useDispatch();
const kwm2Keys = useSelector(selectStagingKWMv2Keys);
const [showImportModal, setShowImportModal] = useState(false);
const addKey = () => dispatch(kwm2AddKey());
const clearAll = () => dispatch(kwm2ClearKeys());
const handleSecretImport = () => {
toast({
title: '尚未实现',
isClosable: true,
status: 'error',
});
};
return (
<Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg">
KwmV2
</Heading>
<Text>
V2 <Code>mflac</Code> 沿 <Code>kwm</Code>{''}
</Text>
<HStack pb={2} pt={2}>
<ButtonGroup isAttached colorScheme="purple" variant="outline">
<Button onClick={addKey} leftIcon={<Icon as={MdAdd} />}>
</Button>
<Menu>
<MenuButton as={IconButton} icon={<MdExpandMore />}></MenuButton>
<MenuList>
<MenuItem onClick={() => setShowImportModal(true)} icon={<Icon as={MdFileUpload} boxSize={5} />}>
</MenuItem>
<MenuDivider />
<MenuItem color="red" onClick={clearAll} icon={<Icon as={MdDeleteForever} boxSize={5} />}>
</MenuItem>
</MenuList>
</Menu>
</ButtonGroup>
</HStack>
<Box flex={1} minH={0} overflow="auto" pr="4">
<List spacing={3}>
{kwm2Keys.map(({ id, ekey, quality, rid }, i) => (
<KWMv2EKeyItem key={id} id={id} ekey={ekey} quality={quality} rid={rid} i={i} />
))}
</List>
{kwm2Keys.length === 0 && <Text></Text>}
</Box>
<ImportSecretModal
clientName="QQ 音乐"
show={showImportModal}
onClose={() => setShowImportModal(false)}
onImport={handleSecretImport}
>
</ImportSecretModal>
</Flex>
);
}

View File

@@ -14,19 +14,33 @@ import {
MenuDivider,
MenuItem,
MenuList,
Tab,
TabList,
TabPanel,
TabPanels,
Text,
Tooltip,
useToast,
} from '@chakra-ui/react';
import { useDispatch, useSelector } from 'react-redux';
import { qmc2AddKey, qmc2AllowFuzzyNameSearch, qmc2ClearKeys } from '../settingsSlice';
import { qmc2AddKey, qmc2AllowFuzzyNameSearch, qmc2ClearKeys, qmc2ImportKeys } from '../settingsSlice';
import { selectStagingQMCv2Settings } from '../settingsSelector';
import React, { useState } from 'react';
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
import { ImportFileModal } from './QMCv2/ImportFileModal';
import { KeyInput } from './QMCv2/KeyInput';
import { QMCv2EKeyItem } from './QMCv2/QMCv2EKeyItem';
import { InfoOutlineIcon } from '@chakra-ui/icons';
import { ImportSecretModal } from '~/components/ImportSecretModal';
import { StagingQMCv2Key } from '../keyFormats';
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor';
import { MMKVParser } from '~/util/MMKVParser';
import { getFileName } from '~/util/pathHelper';
import { InstructionsAndroid } from './QMCv2/InstructionsAndroid';
import { InstructionsIOS } from './QMCv2/InstructionsIOS';
import { InstructionsMac } from './QMCv2/InstructionsMac';
import { InstructionsPC } from './QMCv2/InstructionsPC';
export function PanelQMCv2Key() {
const toast = useToast();
const dispatch = useDispatch();
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
const [showImportModal, setShowImportModal] = useState(false);
@@ -38,6 +52,44 @@ export function PanelQMCv2Key() {
dispatch(qmc2AllowFuzzyNameSearch({ enable: e.target.checked }));
};
const handleSecretImport = async (file: File) => {
try {
const fileBuffer = await file.arrayBuffer();
let qmc2Keys: null | Omit<StagingQMCv2Key, 'id'>[] = null;
if (/[_.]db$/i.test(file.name)) {
const extractor = await DatabaseKeyExtractor.getInstance();
qmc2Keys = extractor.extractQmAndroidDbKeys(fileBuffer);
if (!qmc2Keys) {
alert(`不是支持的 SQLite 数据库文件。\n表名${qmc2Keys}`);
return;
}
} else if (/MMKVStreamEncryptId|filenameEkeyMap/i.test(file.name)) {
const fileBuffer = await file.arrayBuffer();
const map = MMKVParser.toStringMap(new DataView(fileBuffer));
qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey }));
}
if (qmc2Keys) {
dispatch(qmc2ImportKeys(qmc2Keys));
setShowImportModal(false);
toast({
title: `导入成功 (${qmc2Keys.length})`,
description: '记得保存更改来应用。',
isClosable: true,
duration: 5000,
status: 'success',
});
} else {
alert(`不支持的文件:${file.name}`);
}
} catch (e) {
console.error('error during import: ', e);
alert(`导入数据库时发生错误:${e}`);
}
};
return (
<Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg">
@@ -99,14 +151,40 @@ export function PanelQMCv2Key() {
<Box flex={1} minH={0} overflow="auto" pr="4">
<List spacing={3}>
{qmc2Keys.map(({ id, key, name }, i) => (
<KeyInput key={id} id={id} ekey={key} name={name} i={i} />
{qmc2Keys.map(({ id, ekey, name }, i) => (
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
))}
</List>
{qmc2Keys.length === 0 && <Text></Text>}
</Box>
<ImportFileModal show={showImportModal} onClose={() => setShowImportModal(false)} />
<ImportSecretModal
clientName="QQ 音乐"
show={showImportModal}
onClose={() => setShowImportModal(false)}
onImport={handleSecretImport}
>
<TabList>
<Tab></Tab>
<Tab>iOS</Tab>
<Tab>Mac</Tab>
<Tab>Windows</Tab>
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<InstructionsAndroid />
</TabPanel>
<TabPanel>
<InstructionsIOS />
</TabPanel>
<TabPanel>
<InstructionsMac />
</TabPanel>
<TabPanel>
<InstructionsPC />
</TabPanel>
</TabPanels>
</ImportSecretModal>
</Flex>
);
}

View File

@@ -15,10 +15,10 @@ import { qmc2DeleteKey, qmc2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
import { memo } from 'react';
export const KeyInput = memo(({ id, name, ekey, i }: { id: string; name: string; ekey: string; i: number }) => {
export const QMCv2EKeyItem = memo(({ id, name, ekey, i }: { id: string; name: string; ekey: string; i: number }) => {
const dispatch = useAppDispatch();
const updateKey = (prop: 'name' | 'key', e: React.ChangeEvent<HTMLInputElement>) =>
const updateKey = (prop: 'name' | 'ekey', e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(qmc2UpdateKey({ id, field: prop, value: e.target.value }));
const deleteKey = () => dispatch(qmc2DeleteKey({ id }));
@@ -36,7 +36,7 @@ export const KeyInput = memo(({ id, name, ekey, i }: { id: string; name: string;
<InputLeftElement pr="2">
<Icon as={MdVpnKey} />
</InputLeftElement>
<Input variant="flushed" placeholder="密钥" value={ekey} onChange={(e) => updateKey('key', e)} />
<Input variant="flushed" placeholder="密钥" value={ekey} onChange={(e) => updateKey('ekey', e)} />
<InputRightElement>
<Text pl="2" color={ekey.length ? 'green.500' : 'red.500'}>
<code>{ekey.length || '?'}</code>