Compare commits

...

4 Commits

Author SHA1 Message Date
e9a95d1bd6 0.3.2 2024-09-26 21:06:00 +01:00
00813957d6 feat: make unsaved settings obvious 2024-09-26 21:02:42 +01:00
b26e62e8d9 chore: bump version to 0.3.1 2024-09-26 20:43:23 +01:00
9fed1ee610 fix: footer-less qmcv2 file support 2024-09-26 20:41:02 +01:00
4 changed files with 46 additions and 10 deletions

View File

@ -1,7 +1,7 @@
{ {
"name": "um-react", "name": "um-react",
"private": true, "private": true,
"version": "0.3.0", "version": "0.3.2",
"type": "module", "type": "module",
"scripts": { "scripts": {
"start": "vite", "start": "vite",

View File

@ -38,20 +38,32 @@ export class QQMusicV2Decipher implements DecipherInstance {
this.cipherName = `QQMusic/QMC2(user_key=${+useUserKey})`; this.cipherName = `QQMusic/QMC2(user_key=${+useUserKey})`;
} }
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> { parseFooter(buffer: Uint8Array): { size: number; ekey?: undefined | string } {
const footer = QMCFooter.parse(buffer.subarray(buffer.byteLength - 1024)); const footer = QMCFooter.parse(buffer.subarray(buffer.byteLength - 1024));
if (!footer) {
if (footer) {
const { size, ekey } = footer;
footer.free();
return { size, ekey };
}
// No footer, and we don't accept user key:
if (!this.useUserKey) {
throw new UnsupportedSourceFile('Not QMC2 File'); throw new UnsupportedSourceFile('Not QMC2 File');
} }
const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size); return { size: 0 };
}
async decrypt(buffer: Uint8Array, options: DecryptCommandOptions): Promise<DecipherResult | DecipherOK> {
const footer = this.parseFooter(buffer.subarray(buffer.byteLength - 1024));
const ekey = this.useUserKey ? options.qmc2Key : footer.ekey; const ekey = this.useUserKey ? options.qmc2Key : footer.ekey;
footer.free();
if (!ekey) { if (!ekey) {
throw new Error('EKey missing'); throw new Error('EKey required');
} }
const qmc2 = new QMC2(ekey); const qmc2 = new QMC2(ekey);
const audioBuffer = buffer.slice(0, buffer.byteLength - footer.size);
for (const [block, offset] of chunkBuffer(audioBuffer)) { for (const [block, offset] of chunkBuffer(audioBuffer)) {
qmc2.decrypt(block, offset); qmc2.decrypt(block, offset);
} }

View File

@ -1,8 +1,8 @@
import { import {
chakra,
Box, Box,
Button, Button,
Center, Center,
chakra,
Flex, Flex,
HStack, HStack,
Icon, Icon,
@ -19,9 +19,9 @@ import {
TabPanels, TabPanels,
Tabs, Tabs,
Text, Text,
VStack,
useBreakpointValue, useBreakpointValue,
useToast, useToast,
VStack,
} 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';
@ -145,7 +145,7 @@ export function Settings() {
onClick={handleResetSettings} onClick={handleResetSettings}
colorScheme="red" colorScheme="red"
variant="ghost" variant="ghost"
title="放弃未储存的更改,将设定还原储存前的状态。" title="放弃未储存的更改,将设定还原储存前的状态。"
aria-label="放弃未储存的更改" aria-label="放弃未储存的更改"
/> />
<Button onClick={handleApplySettings}></Button> <Button onClick={handleApplySettings}></Button>

View File

@ -1,12 +1,36 @@
import { Box, VStack } from '@chakra-ui/react'; import { Alert, AlertIcon, Box, Button, Flex, Text, VStack } from '@chakra-ui/react';
import { SelectFile } from '../components/SelectFile'; import { SelectFile } from '../components/SelectFile';
import { FileListing } from '~/features/file-listing/FileListing'; import { FileListing } from '~/features/file-listing/FileListing';
import { useAppDispatch, useAppSelector } from '~/hooks.ts';
import { selectIsSettingsNotSaved } from '~/features/settings/settingsSelector.ts';
import { commitStagingChange } from '~/features/settings/settingsSlice.ts';
export function MainTab() { export function MainTab() {
const dispatch = useAppDispatch();
const isSettingsNotSaved = useAppSelector(selectIsSettingsNotSaved);
const onClickSaveSettings = () => {
dispatch(commitStagingChange());
};
return ( return (
<Box h="full" w="full" pt="4"> <Box h="full" w="full" pt="4">
<VStack gap="3"> <VStack gap="3">
{isSettingsNotSaved && (
<Alert borderRadius={7} maxW={400} status="warning">
<AlertIcon />
<Flex flexDir="row" alignItems="center" flexGrow={1} justifyContent="space-between">
<Text m={0}>
<br />
</Text>
<Button type="button" ml={3} size="md" onClick={onClickSaveSettings}>
</Button>
</Flex>
</Alert>
)}
<SelectFile /> <SelectFile />
<Box w="full"> <Box w="full">