import { Box, Button, ButtonGroup, Checkbox, Flex, HStack, Heading, Icon, IconButton, List, Menu, MenuButton, MenuDivider, MenuItem, MenuList, Select, Text, Tooltip, useToast, } from '@chakra-ui/react'; import { useDispatch, useSelector } from 'react-redux'; 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 { 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 { parseAndroidQmEKey } from '~/util/mmkv/qm'; import { getFileName } from '~/util/pathHelper'; import { QMCv2QQMusicAllInstructions } from './QMCv2/QMCv2QQMusicAllInstructions'; import { QMCv2DoubanAllInstructions } from './QMCv2/QMCv2DoubanAllInstructions'; export function PanelQMCv2Key() { const toast = useToast(); const dispatch = useDispatch(); const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings); const [showImportModal, setShowImportModal] = useState(false); const [secretType, setSecretType] = useState<'qm' | 'douban'>('qm'); const addKey = () => dispatch(qmc2AddKey()); const clearAll = () => dispatch(qmc2ClearKeys()); const handleAllowFuzzyNameSearchCheckbox = (e: React.ChangeEvent) => { dispatch(qmc2AllowFuzzyNameSearch({ enable: e.target.checked })); }; const handleSecretImport = async (file: File) => { try { const fileBuffer = await file.arrayBuffer(); let qmc2Keys: null | Omit[] = null; if (/(player_process[_.]db|music_audio_play)(\.db)?$/i.test(file.name)) { const extractor = await DatabaseKeyExtractor.getInstance(); qmc2Keys = extractor.extractQmcV2KeysFromSqliteDb(fileBuffer); if (!qmc2Keys) { alert(`不是支持的 SQLite 数据库文件。`); return; } } else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1/i.test(file.name)) { const fileBuffer = await file.arrayBuffer(); const map = parseAndroidQmEKey(new DataView(fileBuffer)); qmc2Keys = Array.from(map.entries(), ([name, ekey]) => ({ name: getFileName(name), ekey })); } if (qmc2Keys?.length === 0) { toast({ title: '未导入密钥', description: '选择的密钥数据库文件未发现任何可用的密钥。', isClosable: true, status: 'warning', }); } else if (qmc2Keys) { dispatch(qmc2ImportKeys(qmc2Keys)); setShowImportModal(false); toast({ title: `导入成功 (${qmc2Keys.length})`, description: '记得保存更改来应用。', isClosable: true, status: 'success', }); } else { alert(`不支持的文件:${file.name}`); } } catch (e) { console.error('error during import: ', e); alert(`导入数据库时发生错误:${e}`); } }; return ( QMCv2 解密密钥 QQ 音乐、豆瓣 FM 目前采用的加密方案(QMCv2)。在使用「QQ 音乐」安卓、Mac 或 iOS 客户端,以及在使用「豆瓣 FM」安卓客户端的情况下,其「离线加密文件」对应的「密钥」储存在独立的数据库文件内。 }> setShowImportModal(true)} icon={}> 从文件导入密钥… }> 清空密钥 匹配相似文件名 若文件名匹配失败,则使用相似文件名的密钥。 使用「 莱文斯坦距离 ( Levenshtein distance ) 」算法计算相似程度。 若密钥数量过多,匹配时可能会造成浏览器卡顿或无响应一段时间。 若不确定,请勾选该项。 } > {qmc2Keys.map(({ id, ekey, name }, i) => ( ))} {qmc2Keys.length === 0 && 还没有密钥。} setSecretType(e.target.value as 'qm' | 'douban')} variant="flushed" display="inline" css={{ paddingLeft: '0.75rem', width: 'auto' }} > } show={showImportModal} onClose={() => setShowImportModal(false)} onImport={handleSecretImport} > {secretType === 'qm' && } {secretType === 'douban' && } ); }