refactor: batch 3

This commit is contained in:
鲁树人
2025-05-18 02:41:20 +09:00
parent 75b43e1e84
commit 2e4e57be45
52 changed files with 933 additions and 1136 deletions

View File

@@ -1,24 +0,0 @@
import { Box, Image } from '@chakra-ui/react';
import noCoverFallbackImageURL from '~/assets/no-cover.svg';
interface AlbumImageProps {
url?: string;
name?: string;
}
export function AlbumImage({ name, url }: AlbumImageProps) {
const coverAlternativeText = name ? `${name} 的专辑封面` : '专辑封面';
return (
<Box w="160px" h="160px" m="auto">
<Image
border="2px solid"
borderColor="gray.400"
borderRadius="50%"
objectFit="cover"
src={url || noCoverFallbackImageURL}
alt={coverAlternativeText}
/>
</Box>
);
}

View File

@@ -1,5 +1,6 @@
import { Box, Button, chakra, Collapse, Text, useDisclosure } from '@chakra-ui/react';
import { toast } from 'react-toastify';
import { DecryptErrorType } from '~/decrypt-worker/util/DecryptError';
import { applyTemplate } from '~/util/applyTemplate';
export interface FileErrorProps {
error: null | string;
@@ -7,33 +8,55 @@ export interface FileErrorProps {
}
const errorMap = new Map<string | null | DecryptErrorType, string>([
[DecryptErrorType.UNSUPPORTED_FILE, '尚未支持的文件格式'],
[DecryptErrorType.UNSUPPORTED_FILE, '支持的文件类型'],
]);
const ERROR_TEMPLATE = `解密错误:{{summary}}
详细信息:
\`\`\`text
{{error}}
\`\`\`
<!-- 报告错误时请提交上述【全部内容】 -->
`;
export function FileError({ error, code }: FileErrorProps) {
const { isOpen, onToggle } = useDisclosure();
const errorSummary = errorMap.get(code) ?? '未知错误';
const summary = errorMap.get(code) ?? '未知错误';
const copyError = () => {
if (error) {
navigator.clipboard
.writeText(applyTemplate(ERROR_TEMPLATE, { summary, error }))
.then(() => {
toast.success('错误信息已复制到剪贴板');
})
.catch((e) => {
toast.error(`复制错误信息失败: ${e}`);
});
}
};
return (
<Box>
<Text>
<chakra.span>
<chakra.span color="red.700">{errorSummary}</chakra.span>
</chakra.span>
{error && (
<Button ml="2" onClick={onToggle} type="button">
</Button>
)}
</Text>
<>
<p>
<span className="text-red-600">{summary}</span>
</p>
{error && (
<Collapse in={isOpen} animateOpacity>
<Box as="pre" display="inline-block" mt="2" px="4" py="2" bg="red.100" color="red.800" rounded="md">
{error}
</Box>
</Collapse>
<div className="collapse border-error border w-full text-left my-2 py-0">
<input className="[&&&]:py-2 [&&&]:min-h-[1.5rem]" type="checkbox" />
<div className="collapse-title font-semibold text-center [&&&]:min-h-[1.5rem] [&&&]:py-2"></div>
<div className="collapse-content text-sm overflow-hidden">
<pre className="overflow-x-auto w-full">{error}</pre>
<p className="mt-2 text-center">
<button type="button" className="btn btn-secondary" onClick={copyError}>
</button>
</p>
</div>
</div>
)}
</Box>
</>
);
}

View File

@@ -26,7 +26,7 @@ export function FileRow({ id, file }: FileRowProps) {
{metadata?.name ?? nameWithoutExt}
</h2>
<div>
<div className="w-full grow">
{file.state === ProcessState.ERROR && <FileError error={file.errorMessage} code={file.errorCode} />}
{isDecrypted && (
<audio

View File

@@ -1,22 +0,0 @@
import { Box, Text } from '@chakra-ui/react';
import type { AudioMetadata } from './fileListingSlice';
export interface SongMetadataProps {
metadata: AudioMetadata;
}
export function SongMetadata({ metadata }: SongMetadataProps) {
return (
<Box>
<Text>
: <span data-testid="audio-meta-album-name">{metadata.album}</span>
</Text>
<Text>
: <span data-testid="audio-meta-song-artist">{metadata.artist}</span>
</Text>
<Text>
: <span data-testid="audio-meta-album-artist">{metadata.albumArtist}</span>
</Text>
</Box>
);
}

View File

@@ -1,33 +1,28 @@
import { Code, ListItem, OrderedList, Text, chakra } from '@chakra-ui/react';
const KUWO_IOS_DIR = '/var/mobile/Containers/Data/Application/<酷我数据目录>/mmkv';
import { HiWord } from '~/components/HelpText/HiWord';
export function InstructionsIOS() {
return (
<>
<Text>访 iOS </Text>
<Text>
<p>访 iOS </p>
<p>
<chakra.span color="red.400"></chakra.span>
</Text>
<OrderedList>
<ListItem>
<Text>
访
<Code wordBreak="break-word">{KUWO_IOS_DIR}</Code>
</Text>
</ListItem>
<ListItem>
<Text>
<Code>kw_ekey</Code> 访
</Text>
</ListItem>
<ListItem>
<Text>
<Code>kw_ekey</Code>
</Text>
</ListItem>
</OrderedList>
<span className="text-red-600"></span>
</p>
<ol className="list-decimal pl-6">
<li>
访
<br />
<code className="break-words">
/var/mobile/Containers/Data/Application/<HiWord className="text-nowrap">{'<>'}</HiWord>/mmkv
</code>
</li>
<li>
<code>kw_ekey</code> 访
</li>
<li>
<code>kw_ekey</code>
</li>
</ol>
</>
);
}

View File

@@ -1,9 +1,3 @@
import { Text } from '@chakra-ui/react';
export function InstructionsPC() {
return (
<>
<Text>使 Windows </Text>
</>
);
return <p>使 Windows </p>;
}

View File

@@ -1,30 +1,16 @@
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsPC } from './InstructionsPC';
import { InstructionsIOS } from './InstructionsIOS';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs';
export function KWMv2AllInstructions() {
return (
<>
<TabList>
<Tab></Tab>
<Tab>iOS</Tab>
<Tab>Windows</Tab>
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<AndroidADBPullInstruction
dir="/data/data/cn.kuwo.player/files/mmkv"
file="cn.kuwo.player.mmkv.defaultconfig"
/>
</TabPanel>
<TabPanel>
<InstructionsIOS />
</TabPanel>
<TabPanel>
<InstructionsPC />
</TabPanel>
</TabPanels>
</>
);
const ANDROID_DIR = '/data/data/cn.kuwo.player/files/mmkv';
const ANDROID_FILE = 'cn.kuwo.player.mmkv.defaultconfig';
const tabs: InstructionTab[] = [
{ id: 'android', label: '安卓', content: <AndroidADBPullInstruction dir={ANDROID_DIR} file={ANDROID_FILE} /> },
{ id: 'ios', label: 'iOS', content: <InstructionsIOS /> },
{ id: 'windows', label: 'Windows', content: <InstructionsPC /> },
];
return <InstructionsTabs tabs={tabs} />;
}

View File

@@ -1,81 +1,41 @@
import {
HStack,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
ListItem,
Text,
VStack,
} from '@chakra-ui/react';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { PiFileAudio, PiHash } from 'react-icons/pi';
import { kwm2DeleteKey, kwm2UpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
import { memo } from 'react';
import { StagingKWMv2Key } from '../../keyFormats';
import { KeyInput } from '~/components/KeyInput';
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 }));
const ekeyLen = ekey.length;
const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
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="密钥,通常包含 364 或 704 位字符,没有空格。"
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>
<KeyInput
name={rid}
quality={quality}
value={ekey}
isValidKey={isValidEKey}
onSetName={(value) => dispatch(kwm2UpdateKey({ id, field: 'rid', value }))}
onSetQuality={(value) => dispatch(kwm2UpdateKey({ id, field: 'quality', value }))}
onSetValue={(value) => dispatch(kwm2UpdateKey({ id, field: 'ekey', value }))}
onDelete={() => dispatch(kwm2DeleteKey({ id }))}
sequence={i + 1}
nameLabel={
<>
ID
<PiHash className="hidden md:inline-block" />
</>
}
qualityLabel={
<>
<PiFileAudio className="hidden md:inline-block" />
</>
}
namePlaceholder="音频哈希。不建议手动填写。"
qualityPlaceholder="比特率 ID"
valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
/>
);
});

View File

@@ -1,34 +1,50 @@
import { Code, Heading, ListItem, OrderedList, Text } from '@chakra-ui/react';
import { RiFileCopyLine } from 'react-icons/ri';
import { toast } from 'react-toastify';
import { ExtLink } from '~/components/ExtLink';
import { FilePathBlock } from '~/components/FilePathBlock.tsx';
export function InstructionsPC() {
const DB_PATH = '%APPDATA%\\KuGou8\\KGMusicV3.db';
const copyDbPathToClipboard = () => {
navigator.clipboard
.writeText(DB_PATH)
.then(() => {
toast.success('已复制到剪贴板');
})
.catch((err) => {
toast.error(`复制失败,请手动复制\n${err}`);
});
};
return (
<>
<Text> Windows 使 <abbr title="SQLite w/ SQLCipher">SQLite</abbr> </Text>
<Text></Text>
<FilePathBlock>%APPDATA%\KuGou8\KGMusicV3.db</FilePathBlock>
<p>
Windows 使
<ExtLink className="link-info px-1" href="https://www.zetetic.net/sqlcipher/">
SQLCipher
</ExtLink>
</p>
<p></p>
<p className="flex items-center gap-1">
<FilePathBlock>{DB_PATH}</FilePathBlock>
</p>
<Heading as="h3" size="md" mt="4">
</Heading>
<OrderedList>
<ListItem>
<Text>
<Code>KGMusicV3.db</Code>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
<ListItem>
<Text>
<Code>KGMusicV3.db</Code>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
</OrderedList>
<h3 className="font-bold text-xl mt-4"></h3>
<ol className="list-decimal pl-6">
<li>
<button className="btn btn-sm btn-outline btn-accent mr-2" onClick={copyDbPathToClipboard}>
<RiFileCopyLine className="text-xl" />
<span></span>
</button>
<code>KGMusicV3.db</code>
</li>
<li></li>
<li>
<code>KGMusicV3.db</code>
</li>
<li></li>
</ol>
</>
);
}

View File

@@ -1,25 +1,14 @@
import { Tab, TabList, TabPanel, TabPanels } from '@chakra-ui/react';
import { AndroidADBPullInstruction } from '~/components/AndroidADBPullInstruction/AndroidADBPullInstruction';
import { InstructionsPC } from './InstructionsPC';
import { InstructionsTabs, InstructionTab } from '~/components/InstructionsTabs';
export function KugouAllInstructions() {
return (
<>
<TabList>
<Tab></Tab>
<Tab>Windows</Tab>
</TabList>
<TabPanels flex={1} overflow="auto">
<TabPanel>
<AndroidADBPullInstruction
dir="/data/data/com.kugou.android/files/mmkv"
file="mggkey_multi_process"
/>
</TabPanel>
<TabPanel>
<InstructionsPC />
</TabPanel>
</TabPanels>
</>
);
const ANDROID_DIR = '/data/data/com.kugou.android/files/mmkv';
const ANDROID_FILE = 'mggkey_multi_process';
const tabs: InstructionTab[] = [
{ id: 'android', label: '安卓', content: <AndroidADBPullInstruction dir={ANDROID_DIR} file={ANDROID_FILE} /> },
{ id: 'windows', label: 'Windows', content: <InstructionsPC /> },
];
return <InstructionsTabs tabs={tabs} />;
}

View File

@@ -1,72 +1,26 @@
import {
HStack,
Icon,
IconButton,
Input,
InputGroup,
InputLeftElement,
InputRightElement,
ListItem,
Text,
VStack,
} from '@chakra-ui/react';
import { MdDelete, MdVpnKey } from 'react-icons/md';
import { kugouDeleteKey, kugouUpdateKey } from '../../settingsSlice';
import { useAppDispatch } from '~/hooks';
import { memo } from 'react';
import { StagingKugouKey } from '../../keyFormats';
import { KeyInput } from '~/components/KeyInput';
export const KugouEKeyItem = memo(({ id, ekey, audioHash, i }: StagingKugouKey & { i: number }) => {
const dispatch = useAppDispatch();
const updateKey = (prop: keyof StagingKugouKey, e: React.ChangeEvent<HTMLInputElement>) =>
dispatch(kugouUpdateKey({ id, field: prop, value: e.target.value }));
const deleteKey = () => dispatch(kugouDeleteKey({ id }));
const ekeyLen = ekey.length;
const isValidEKey = ekeyLen === 364 || ekeyLen === 704;
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="音频哈希。不建议手动填写。"
value={audioHash}
onChange={(e) => updateKey('audioHash', e)}
/>
</HStack>
<InputGroup size="xs">
<InputLeftElement pr="2">
<Icon as={MdVpnKey} />
</InputLeftElement>
<Input
variant="flushed"
placeholder="密钥,通常包含 364 或 704 位字符,没有空格。"
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>
<KeyInput
name={audioHash}
value={ekey}
isValidKey={isValidEKey}
onSetName={(value) => dispatch(kugouUpdateKey({ id, field: 'audioHash', value }))}
onSetValue={(value) => dispatch(kugouUpdateKey({ id, field: 'ekey', value }))}
onDelete={() => dispatch(kugouDeleteKey({ id }))}
sequence={i + 1}
namePlaceholder="音频哈希。不建议手动填写。"
valuePlaceholder="密钥,通常包含 364 或 704 位字符,没有空格。"
/>
);
});

View File

@@ -1,5 +1,4 @@
import { Box, Flex, Heading, List, Text, useToast } from '@chakra-ui/react';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { ImportSecretModal } from '~/components/ImportSecretModal';
@@ -12,9 +11,10 @@ import { KugouEKeyItem } from '~/features/settings/panels/Kugou/KugouEKeyItem.ts
import { KugouAllInstructions } from '~/features/settings/panels/Kugou/KugouAllInstructions.tsx';
import { parseAndroidKugouMMKV } from '~/util/mmkv/kugou.ts';
import { DatabaseKeyExtractor } from '~/util/DatabaseKeyExtractor.ts';
import { KeyListContainer } from '~/components/KeyListContainer';
import { toastImportResult } from '~/util/toastImportResult';
export function PanelKGGKey() {
const toast = useToast();
const dispatch = useDispatch();
const kugouKeys = useSelector(selectStagingKugouV5Keys);
const [showImportModal, setShowImportModal] = useState(false);
@@ -30,49 +30,34 @@ export function PanelKGGKey() {
keys = extractor.extractKugouKeyFromEncryptedDb(await file.arrayBuffer());
}
if (keys?.length === 0) {
toast({
title: '未导入密钥',
description: '选择的密钥数据库文件未发现任何可用的密钥。',
isClosable: true,
status: 'warning',
});
} else if (keys) {
if (keys && keys.length > 0) {
dispatch(kugouImportKeys(keys));
setShowImportModal(false);
toast({
title: `导入完成,共导入了 ${keys.length} 个密钥。`,
description: '记得按下「保存」来应用。',
isClosable: true,
status: 'success',
});
} else {
toast({
title: `不支持的文件:${file.name}`,
isClosable: true,
status: 'error',
});
}
toastImportResult(file.name, keys);
};
const refKeyContainer = useRef<HTMLDivElement>(null);
return (
<Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg">
(KGG / KGM v5)
</Heading>
<div className="container flex flex-col grow min-h-0 w-full">
<h2 className="text-2xl font-bold"> (KGG / KGM v5)</h2>
<Text>使 KGG / KGM v5 </Text>
<p>使 KGG / KGM v5 </p>
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} />
<h3 className="mt-2 text-xl font-bold"></h3>
<AddKey
addKey={addKey}
refContainer={refKeyContainer}
importKeyFromFile={() => setShowImportModal(true)}
clearKeys={clearAll}
/>
<Box flex={1} minH={0} overflow="auto" pr="4">
<List spacing={3}>
{kugouKeys.map(({ id, audioHash, ekey }, i) => (
<KugouEKeyItem key={id} id={id} ekey={ekey} audioHash={audioHash} i={i} />
))}
</List>
{kugouKeys.length === 0 && <Text></Text>}
</Box>
<KeyListContainer ref={refKeyContainer} keys={kugouKeys}>
{kugouKeys.map(({ id, audioHash, ekey }, i) => (
<KugouEKeyItem key={id} id={id} ekey={ekey} audioHash={audioHash} i={i} />
))}
</KeyListContainer>
<ImportSecretModal
clientName="酷狗音乐"
@@ -82,6 +67,6 @@ export function PanelKGGKey() {
>
<KugouAllInstructions />
</ImportSecretModal>
</Flex>
</div>
);
}

View File

@@ -1,25 +1,5 @@
import {
Box,
Button,
ButtonGroup,
Code,
Flex,
HStack,
Heading,
Icon,
IconButton,
List,
Menu,
MenuButton,
MenuDivider,
MenuItem,
MenuList,
Text,
useToast,
} from '@chakra-ui/react';
import { useState } from 'react';
import { useRef, useState } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { MdAdd, MdDeleteForever, MdExpandMore, MdFileUpload } from 'react-icons/md';
import { ImportSecretModal } from '~/components/ImportSecretModal';
import { parseAndroidKuwoEKey, parseIosKuwoEKey } from '~/util/mmkv/kuwo';
@@ -29,9 +9,11 @@ import { selectStagingKWMv2Keys } from '../settingsSelector';
import { KWMv2EKeyItem } from './KWMv2/KWMv2EKeyItem';
import type { StagingKWMv2Key } from '../keyFormats';
import { KWMv2AllInstructions } from './KWMv2/KWMv2AllInstructions';
import { AddKey } from '~/components/AddKey';
import { KeyListContainer } from '~/components/KeyListContainer';
import { toastImportResult } from '~/util/toastImportResult';
export function PanelKWMv2Key() {
const toast = useToast();
const dispatch = useDispatch();
const kwm2Keys = useSelector(selectStagingKWMv2Keys);
const [showImportModal, setShowImportModal] = useState(false);
@@ -46,70 +28,33 @@ export function PanelKWMv2Key() {
keys = parseIosKuwoEKey(new DataView(await file.arrayBuffer()));
}
if (keys?.length === 0) {
toast({
title: '未导入密钥',
description: '选择的密钥数据库文件未发现任何可用的密钥。',
isClosable: true,
status: 'warning',
});
} else if (keys) {
if (keys && keys.length > 0) {
dispatch(kwm2ImportKeys(keys));
setShowImportModal(false);
toast({
title: `导入完成,共导入了 ${keys.length} 个密钥。`,
description: '记得按下「保存」来应用。',
isClosable: true,
status: 'success',
});
} else {
toast({
title: `不支持的文件:${file.name}`,
isClosable: true,
status: 'error',
});
}
toastImportResult(file.name, keys);
};
const refKeyContainer = useRef<HTMLDivElement>(null);
return (
<Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg">
KwmV2
</Heading>
<div className="container flex flex-col grow min-h-0 w-full">
<h2 className="text-2xl font-bold">KwmV2</h2>
<p>
V2 <code>mflac</code> <code>mgg</code>
</p>
<p></p>
<AddKey
addKey={addKey}
refContainer={refKeyContainer}
importKeyFromFile={() => setShowImportModal(true)}
clearKeys={clearAll}
/>
<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>
<KeyListContainer ref={refKeyContainer} keys={kwm2Keys}>
{kwm2Keys.map(({ id, ekey, quality, rid }, i) => (
<KWMv2EKeyItem key={id} id={id} ekey={ekey} quality={quality} rid={rid} i={i} />
))}
</KeyListContainer>
<ImportSecretModal
clientName="酷我音乐"
@@ -119,6 +64,6 @@ export function PanelKWMv2Key() {
>
<KWMv2AllInstructions />
</ImportSecretModal>
</Flex>
</div>
);
}

View File

@@ -1,8 +1,7 @@
import { 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 React, { useRef, useState } from 'react';
import { QMCv2EKeyItem } from './QMCv2/QMCv2EKeyItem';
import { ImportSecretModal } from '~/components/ImportSecretModal';
import { StagingQMCv2Key } from '../keyFormats';
@@ -15,9 +14,10 @@ import { AddKey } from '~/components/AddKey';
import { InfoModal } from '~/components/InfoModal.tsx';
import { Ruby } from '~/components/Ruby.tsx';
import { ExtLink } from '~/components/ExtLink.tsx';
import { KeyListContainer } from '~/components/KeyListContainer';
import { toastImportResult } from '~/util/toastImportResult';
export function PanelQMCv2Key() {
const toast = useToast();
const dispatch = useDispatch();
const { keys: qmc2Keys, allowFuzzyNameSearch } = useSelector(selectStagingQMCv2Settings);
const [showImportModal, setShowImportModal] = useState(false);
@@ -34,46 +34,31 @@ export function PanelQMCv2Key() {
try {
const fileBuffer = await file.arrayBuffer();
let qmc2Keys: null | Omit<StagingQMCv2Key, 'id'>[] = null;
let keys: null | Omit<StagingQMCv2Key, 'id'>[] = 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;
}
keys = extractor.extractQmcV2KeysFromSqliteDb(fileBuffer);
} else if (/MMKVStreamEncryptId|filenameEkeyMap|qmpc-mmkv-v1|(\.mmkv$)/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 }));
keys = 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));
if (keys && keys.length > 0) {
dispatch(qmc2ImportKeys(keys));
setShowImportModal(false);
toast({
title: `导入成功 (${qmc2Keys.length})`,
description: '记得保存更改来应用。',
isClosable: true,
status: 'success',
});
} else {
alert(`不支持的文件:${file.name}`);
}
toastImportResult(file.name, keys);
} catch (e) {
console.error('error during import: ', e);
alert(`导入数据库时发生错误:${e}`);
}
};
const refKeyContainer = useRef<HTMLDivElement>(null);
return (
<>
<h2 className="text-2xl font-bold">QMCv2 </h2>
@@ -112,19 +97,19 @@ export function PanelQMCv2Key() {
</InfoModal>
</div>
<h3 className="mt-2 text-1xl font-bold"></h3>
<AddKey addKey={addKey} importKeyFromFile={() => setShowImportModal(true)} clearKeys={clearAll} />
<h3 className="mt-2 text-xl font-bold"></h3>
<AddKey
addKey={addKey}
refContainer={refKeyContainer}
importKeyFromFile={() => setShowImportModal(true)}
clearKeys={clearAll}
/>
<div className="flex-1 min-h-0 overflow-auto pr-4 pt-3">
{qmc2Keys.length > 0 && (
<ul className="list bg-base-100 rounded-box shadow-md border border-base-300">
{qmc2Keys.map(({ id, ekey, name }, i) => (
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
))}
</ul>
)}
{qmc2Keys.length === 0 && <p className="p-4 pb-2 tracking-wide"></p>}
</div>
<KeyListContainer ref={refKeyContainer} keys={qmc2Keys}>
{qmc2Keys.map(({ id, ekey, name }, i) => (
<QMCv2EKeyItem key={id} id={id} ekey={ekey} name={name} i={i} />
))}
</KeyListContainer>
<ImportSecretModal
clientName={

View File

@@ -1,26 +1,14 @@
import {
Box,
Code,
Flex,
FormControl,
FormHelperText,
FormLabel,
Heading,
Input,
ListItem,
Text,
UnorderedList,
} from '@chakra-ui/react';
import { useAppDispatch, useAppSelector } from '~/hooks';
import { ExtLink } from '~/components/ExtLink';
import { ChangeEvent, ClipboardEvent } from 'react';
import { ChangeEvent, ClipboardEvent, useId } from 'react';
import { VQuote } from '~/components/HelpText/VQuote';
import { selectStagingQtfmAndroidKey } from '../settingsSelector';
import { qtfmAndroidUpdateKey } from '../settingsSlice';
import { workerClientBus } from '~/decrypt-worker/client.ts';
import { GetQingTingFMDeviceKeyPayload } from '~/decrypt-worker/types.ts';
import { DECRYPTION_WORKER_ACTION_NAME } from '~/decrypt-worker/constants.ts';
import { Ruby } from '~/components/Ruby';
import { HiWord } from '~/components/HelpText/HiWord';
const QTFM_DEVICE_ID_URL = 'https://github.com/parakeet-rs/qtfm-device-id/releases/latest';
@@ -64,64 +52,72 @@ export function PanelQingTing() {
setSecretKey(e.target.value);
};
const idSecretKey = useId();
return (
<Flex minH={0} flexDir="column" flex={1}>
<Heading as="h2" size="lg">
<div className="min-h-0 flex-col grow px-1">
<h2 className="text-2xl font-bold mb-4">
<VQuote> FM</VQuote>
</Heading>
</h2>
<Text>
<p>
<VQuote> FM</VQuote>
</Text>
<Box mt={3} mb={3}>
<FormControl>
<FormLabel></FormLabel>
<Input type="text" onPaste={handleDataPaste} value={secretKey} onChange={handleDataInput} />
<FormHelperText>
{'粘贴含有设备密钥的信息的内容时将自动提取密钥(如通过 '}
</p>
<div className="my-4">
<fieldset className="fieldset">
<legend className="fieldset-legend text-lg">
<label htmlFor={idSecretKey}></label>
</legend>
<input
id={idSecretKey}
type="text"
className="input font-mono"
onPaste={handleDataPaste}
value={secretKey}
onChange={handleDataInput}
/>
<p className="label">
<ExtLink href={QTFM_DEVICE_ID_URL}>
<Code>qtfm-device-id</Code>
<code>qtfm-device-id</code>
</ExtLink>
{' 获取的设备信息)。'}
</FormHelperText>
</FormControl>
</Box>
root
</p>
</fieldset>
</div>
<Heading as="h3" size="md" pt={3} pb={2}>
</Heading>
<UnorderedList>
<ListItem>
<Text>
<h3 className="text-xl font-bold my-2"></h3>
<ul className="list-disc pl-6">
<li>
<p>
<Code>[]/Android/data/fm.qingting.qtradio/files/Music/</Code>
</Text>
<UnorderedList>
<ListItem>
<Text>
<VQuote>
<code>
<HiWord>[]</HiWord>/Android/data/fm.qingting.qtradio/files/Music/
</code>
</VQuote>
</p>
<ul className="list-disc pl-6">
<li>
<p>
使
<ruby>
<rp> (</rp>
<rt>root</rt>
<rp>)</rp>
</ruby>
<Ruby caption="root"></Ruby>
访
</Text>
</ListItem>
</UnorderedList>
</ListItem>
<ListItem>
<Text>
<Code>.p~!</Code>
</Text>
</ListItem>
<ListItem>
<Text></Text>
</ListItem>
</UnorderedList>
</Flex>
</p>
</li>
</ul>
</li>
<li>
<p>
<code>.p~!</code>
</p>
</li>
<li>
<p></p>
</li>
</ul>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Heading, Text, Code, Kbd, OrderedList, ListItem, Link } from '@chakra-ui/react';
import { Heading, Text, Code, OrderedList, ListItem, Link } from '@chakra-ui/react';
import { FilePathBlock } from '~/components/FilePathBlock';
import { MacCommandKey } from '~/components/Key/MacCommandKey';
import { ShiftKey } from '~/components/Key/ShiftKey';
@@ -42,7 +42,7 @@ export function InstructionsMac() {
{' + '}
<MacCommandKey />
{' + '}
<Kbd>{'G'}</Kbd>
<kbd className="kbd">{'G'}</kbd>
</Text>
</ListItem>
<ListItem>

View File

@@ -1,16 +1,57 @@
import { ExtLink } from '~/components/ExtLink';
import { HiWord } from '~/components/HelpText/HiWord';
import NoopExecutable from './assets/noop.exe?base64';
import NoopExecutableSource from './assets/noop.asm.txt?base64';
export function InstructionsPC() {
return (
<>
<p>
使 <span className="text-primary">19.51 </span>
<mark></mark>
</p>
<p className="mt-2">
使 <span className="text-error">19.57 </span>
<mark></mark>
<HiWord></HiWord>
<br />
</p>
<p className="mt-4">
使 <span className="text-primary">19.51 </span>
<HiWord></HiWord>
</p>
<p className="mt-4">
QQ Windows <HiWord>19.51</HiWord>
</p>
<ul className="list-disc pl-6">
<li>
<ExtLink href="https://web.archive.org/web/2023/https://dldir1v6.qq.com/music/clntupate/QQMusic_Setup_1951.exe">
<code>Archive.org</code>
</ExtLink>
</li>
<li>
<ExtLink href="https://t.me/um_lsr_ch/24"> Telegram </ExtLink>
</li>
</ul>
<p className="mt-4">
QQ
<a
className="link px-1"
download="QQMusicUp.exe"
href={`data:application/vnd.microsoft.portable-executable;base64,${NoopExecutable}`}
>
<code>QQMusicUp.exe</code>
</a>
<a
className="link px-1"
download="QQMusicUp.asm"
href={`data:text/x-asm;charset=utf-8;base64,${NoopExecutableSource}`}
>
</a>
</p>
<p className="mt-2">使</p>
</>
);
}

View File

@@ -0,0 +1,16 @@
; QQ 音乐更新 - 占位文件
; 使用 FASM 编译即可。
format PE GUI 4.0
entry start
include 'win32a.inc'
section '.text' code readable executable
start:
invoke ExitProcess, 0
section '.idata' import data readable writeable
library kernel,'KERNEL32.DLL'
import kernel,\
ExitProcess,'ExitProcess'

Binary file not shown.